Compare commits

...

1 Commits

Author SHA1 Message Date
Ke Sun
5b13b4a949 GitHub release (#20)
* feat(web): remove mock data
* feat(knowledgeBase): Refactor document list API and improve polling logic

- Update getDocumentList API to accept kb_id as separate parameter instead of extracting from query object
- Fix parameter name from auto_question to auto_questions in parser config
- Add progress field initialization in document update params
- Improve polling logic to handle both auto-return and manual stay scenarios with proper loading state management
- Add console logging for debugging polling status and document processing
- Reduce polling interval from 5000ms to 3000ms for faster status updates
- Enhance cleanup logic with route change detection to prevent memory leaks
- Add record parameter to progress render function for better data access
- Refactor confirm dialog callbacks to properly manage loading state timing
- Ensure loading indicator displays correctly when user chooses to stay on page

* feat(web): Add Workflow

* feat(web): Workflow

* feat(web): node show id; update reflection engine example

* feat(components): Add markdown editing capability and enhance component styling

- Add editable mode to Markdown component with edit/save/cancel buttons
- Import EditOutlined, SaveOutlined, CloseOutlined icons from ant-design
- Add useState, useRef, useEffect hooks for managing edit state
- Add editable, onContentChange, and onSave props to RbMarkdownProps interface
- Create RbModal component with new index.css stylesheet for modal styling
- Add index.css stylesheet to KnowledgeBase components for consistent styling
- Update i18n translations in en.ts and zh.ts for new UI elements
- Refactor Markdown component handlers to accept and spread additional props
- Update InsertModal and RecallTestResult components for improved UX
- Fix prop spreading in component handlers to maintain compatibility with Ant Design components

* feat(web): Graph user memory update

* feat(web): update routes.json

* fix(web): workflow bug

* fix(web): workflow variable

* fix(web): workflow properties

* feat(web): workflow support lexical editor

* feat(web): workflow support lexical editor

* feat(web): update reflection engine result

* feat(web): workflow's chat support abort output

* fix:git commit

* fix:vite config

* fix:breadcrumbs

* feat(i18n): add document processing confirmation dialog translations

- Add "processingDocuments" translation key for loading state message in English and Chinese
- Add "startUploadConfirmTitle" translation for confirmation dialog title
- Add "startUploadConfirmContent" translation for confirmation dialog description
- Add "returnToList" translation for returning to list page action
- Add "stayOnPage" translation for staying on current page action
- Support user choice to either return to list or stay on page during background document processing

* fix(web): user memory detail

* feat(web): order

* fix:面包屑修改

* feat(web): 1. user memory; 2. update workspace's model config

* feat(web): update zh.ts / en.ts

* fix(web): update user profile

* feat(web): Agent add ai prompt

* feat(web): Agent add ai prompt

* feat(web): add pricing menu

* feat(knowledgeBase): add media file validation and PDF enhancement method selection

- Add i18n translations for file size and duration validation errors in English and Chinese
- Implement media file validation with 256MB size limit and 150-second duration limit
- Add support for audio and video file formats (mp3, mp4, mov, wav) in dataset creation
- Add checkMediaDuration helper function to validate media file duration using HTML5 media API
- Add PDF enhancement method selection dropdown with options (DeepDoc, MinerU, TextLN)
- Change default PDF enhancement setting from disabled to enabled
- Update file type array to include media formats
- Add error messaging for file size and duration validation failures
- Improve UI spacing for file parsing settings section

* feat(knowledgeBase): add media dataset support and improve file handling

- Add media dataset translations in English and Chinese locales
- Add "mediaDataSet" and "uploadMedia" i18n keys for UI labels
- Enable media dataset creation option in Private component by uncommenting menu item
- Import and display image icon for media dataset menu option
- Refactor file ID handling in CreateDataset to support both string and array types
- Improve fileIds initialization logic to handle mixed input types
- Update CreateImageDataset component to use file chunking workflow
- Add navigation to parameter settings step after file upload
- Pass file IDs to dataset creation flow for media processing
- Add message API and navigate hook for improved UX feedback

* fix(knowledgeBase): improve navigation and folder tree refresh logic

- Add path comparison check in breadcrumb navigation to avoid unnecessary route changes when already on target page
- Implement delayed folder tree refresh with setTimeout to ensure state reset completes before refreshing
- Add manual table refresh trigger to ensure data updates after navigation
- Reset expanded keys in FolderTree component during load to ensure consistent state from root directory
- Add expanded keys reset in breadcrumb navigation to prevent stale expansion state
- Improve navigation state handling by using replace flag only when on target path to reduce history stack pollution

* fix:pdfEnhancementEnabled

* feat(web): add tool management

* fix(web): get the parent domain name adaptation IP

* fix(web): Conversation add initialValue

* feat(web): workflow’s Editor Variable support Tag

* fix(web): pricing UI

* feat(web): JSON Tool update

* fix(web): update get llm,chat model list function

* fix(web): time tool / cluster chat

* fix(web): time tool add time zone

* feat(web): neo4j type user memory detail

* fix(web): update parseSchema api param

* feat: workflow add knowledge-retrieval node

* feat(knowledgeBase): enhance file upload and dataset creation with abort support and improved UX

- Add AbortSignal support to uploadFile API for cancellable uploads
- Implement custom onRemove callback in UploadFiles component with confirmation dialog
- Add i18n translations for file removal confirmation and error messages
- Update supported file types documentation to include IMAGE and MEDIA formats
- Improve file removal UI with cursor pointer styling
- Refactor getModelList API to remove unused type parameter
- Add Form import and UploadFile type for better type safety in CreateDataset
- Enhance error handling and user feedback for file operations

* feat(web): MCP add bearer token auth type

* fix(web): UI update

---------

Co-authored-by: zhaoying <yzhao96@best-inc.com>
Co-authored-by: yujiangping <yujiangping@taofen8.com>
Co-authored-by: 赵莹 <zhaoying@redbearai.com>
Co-authored-by: vrhs@163.com <accounts_660b6454a0eb398d3f8d2c76@mail.teambition.com>
2025-12-30 18:37:40 +08:00
225 changed files with 18068 additions and 2461 deletions

2
web/.gitignore vendored
View File

@@ -22,5 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
vite.config.js
package-lock.json

View File

@@ -10,10 +10,18 @@
"preview": "vite preview"
},
"dependencies": {
"@antv/layout": "^1.2.14-beta.8",
"@antv/x6": "^3.0.1",
"@antv/x6-react-shape": "^3.0.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@lexical/code": "^0.39.0",
"@lexical/link": "^0.39.0",
"@lexical/list": "^0.39.0",
"@lexical/react": "^0.39.0",
"@lexical/rich-text": "^0.39.0",
"antd": "^5.27.4",
"axios": "^1.12.2",
"clsx": "^2.1.1",
@@ -23,6 +31,8 @@
"echarts": "^5.6.0",
"echarts-for-react": "^3.0.2",
"i18next": "^25.6.0",
"js-yaml": "^4.1.1",
"lexical": "^0.39.0",
"mermaid": "^11.12.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -31,7 +41,6 @@
"react-markdown": "^10.1.0",
"react-router-dom": "^6.22.0",
"react-syntax-highlighter": "^16.1.0",
"reactflow": "^11.11.4",
"rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0",
"remark-breaks": "^4.0.0",
@@ -46,6 +55,7 @@
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.14",
"@types/crypto-js": "^4.2.2",
"@types/js-yaml": "^4.0.9",
"@types/node": "^24.6.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",

View File

@@ -6,22 +6,31 @@
// biome-ignore lint: disable
export {}
declare global {
const Activity: typeof import('react').Activity
const Fragment: typeof import('react').Fragment
const Link: typeof import('react-router-dom').Link
const NavLink: typeof import('react-router-dom').NavLink
const Navigate: typeof import('react-router-dom').Navigate
const Outlet: typeof import('react-router-dom').Outlet
const Route: typeof import('react-router-dom').Route
const Routes: typeof import('react-router-dom').Routes
const Suspense: typeof import('react').Suspense
const cache: typeof import('react').cache
const cacheSignal: typeof import('react').cacheSignal
const createContext: typeof import('react').createContext
const createRef: typeof import('react').createRef
const forwardRef: typeof import('react').forwardRef
const lazy: typeof import('react').lazy
const memo: typeof import('react').memo
const startTransition: typeof import('react').startTransition
const use: typeof import('react').use
const useActionState: typeof import('react').useActionState
const useCallback: typeof import('react').useCallback
const useContext: typeof import('react').useContext
const useDebugValue: typeof import('react').useDebugValue
const useDeferredValue: typeof import('react').useDeferredValue
const useEffect: typeof import('react').useEffect
const useEffectEvent: typeof import('react').useEffectEvent
const useHref: typeof import('react-router-dom').useHref
const useId: typeof import('react').useId
const useImperativeHandle: typeof import('react').useImperativeHandle
@@ -33,6 +42,7 @@ declare global {
const useMemo: typeof import('react').useMemo
const useNavigate: typeof import('react-router-dom').useNavigate
const useNavigationType: typeof import('react-router-dom').useNavigationType
const useOptimistic: typeof import('react').useOptimistic
const useOutlet: typeof import('react-router-dom').useOutlet
const useOutletContext: typeof import('react-router-dom').useOutletContext
const useParams: typeof import('react-router-dom').useParams

View File

@@ -27,12 +27,21 @@ import 'dayjs/locale/en'
import 'dayjs/locale/zh-cn'
import 'dayjs/plugin/timezone'
import 'dayjs/plugin/utc'
import { cookieUtils } from './utils/request';
function App() {
const { t } = useTranslation();
const { locale, language, timeZone } = useI18n()
useEffect(() => {
const authToken = cookieUtils.get('authToken')
if (!authToken && !window.location.hash.includes('#/login')) {
window.location.href = `/#/login`;
}
}, [])
useEffect(() => {
document.title = t('memoryBear')

33
web/src/api/apiKey.ts Normal file
View File

@@ -0,0 +1,33 @@
import { request } from '@/utils/request'
import type { ApiKey } from '@/views/ApiKeyManagement/types'
// API Key列表
export const getApiKeyListUrl = '/apikeys'
export const getApiKeyList = (data: Record<string, unknown>) => {
return request.get(getApiKeyListUrl, data)
}
// API Key详情
export const getApiKey = (id: string) => {
return request.get(`/apikeys/${id}`)
}
// 创建API Key
export const createApiKey = (values: ApiKey) => {
return request.post('/apikeys', values)
}
// 更新API Key
export const updateApiKey = (id: string, values: ApiKey) => {
return request.put(`/apikeys/${id}`, values)
}
// 删除 API Key
export const deleteApiKey = (id: string) => {
return request.delete(`/apikeys/${id}`)
}
// 使用统计
export const getApiKeyStats = (app_key_id: string) => {
return request.get(`/apikeys/${app_key_id}/stats`)
}

View File

@@ -1,7 +1,9 @@
import { request } from '@/utils/request'
import type { Application } from '@/views/ApplicationManagement/types'
import type { ApplicationModalData } from '@/views/ApplicationManagement/types'
import type { Config } from '@/views/ApplicationConfig/types'
import { handleSSE } from '@/utils/stream'
import { handleSSE, type SSEMessage } from '@/utils/stream'
import type { QueryParams } from '@/views/Conversation/types'
import type { WorkflowConfig } from '@/views/Workflow/types'
// 应用列表
export const getApplicationListUrl = '/apps'
@@ -12,20 +14,24 @@ export const getApplicationList = (data: Record<string, unknown>) => {
export const getApplicationConfig = (id: string) => {
return request.get(`/apps/${id}/config`)
}
// 获取集群应配置
// 获取集群应配置
export const getMultiAgentConfig = (id: string) => {
return request.get(`/apps/${id}/multi-agent`)
}
// 获取 workflow应用配置
export const getWorkflowConfig = (id: string) => {
return request.get(`/apps/${id}/workflow`)
}
// 应用详情
export const getApplication = (id: string) => {
return request.get(`/apps/${id}`)
}
// 更新应用
export const updateApplication = (id: string, values: Application) => {
export const updateApplication = (id: string, values: ApplicationModalData) => {
return request.put(`/apps/${id}`, values)
}
// 创建应用
export const addApplication = (values: Application) => {
export const addApplication = (values: ApplicationModalData) => {
return request.post('/apps', values)
}
// 保存Agent配置
@@ -36,11 +42,15 @@ export const saveAgentConfig = (app_id: string, values: Config) => {
export const saveMultiAgentConfig = (app_id: string, values: Config) => {
return request.put(`/apps/${app_id}/multi-agent`, values)
}
// 保存workflow配置
export const saveWorkflowConfig = (app_id: string, values: WorkflowConfig) => {
return request.put(`/apps/${app_id}/workflow`, values)
}
// 模型比对试运行
export const runCompare = (app_id: string, values: Record<string, unknown>, onMessage?: (data: string) => void) => {
export const runCompare = (app_id: string, values: Record<string, unknown>, onMessage?: (data: SSEMessage[]) => void) => {
return handleSSE(`/apps/${app_id}/draft/run/compare`, values, onMessage)
}
export const draftRun = (app_id: string, values: Record<string, unknown>, onMessage?: (data: string) => void) => {
export const draftRun = (app_id: string, values: Record<string, unknown>, onMessage?: (data: SSEMessage[]) => void) => {
return handleSSE(`/apps/${app_id}/draft/run`, values, onMessage)
}
// 删除应用
@@ -76,18 +86,7 @@ export const getConversationHistory = (share_token: string, data: { page: number
})
}
// 发送体验对话
export const sendConversation = (share_token: string, values: {
message: string;
web_search: boolean;
memory: boolean;
stream: boolean;
conversation_id: string | null;
}, onMessage, shareToken: string) => {
// return request.post(`/public/share/chat`, values, {
// headers: {
// 'Authorization': `Bearer ${localStorage.getItem(`shareToken_${share_token}`)}`
// }
// })
export const sendConversation = (values: QueryParams, onMessage: (data: SSEMessage[]) => void, shareToken: string) => {
return handleSSE(`/public/share/chat`, values, onMessage, {
headers: {
'Authorization': `Bearer ${shareToken}`

View File

@@ -64,8 +64,8 @@ export const getModelTypeList = async () => {
return response as any[];
};
// 获取模型列表
export const getModelList = async (type: string | string[], pageInfo: PageRequest) => {
const response = await request.get(`${apiPrefix}/models`, { type, ...pageInfo });
export const getModelList = async (pageInfo: PageRequest) => {
const response = await request.get(`${apiPrefix}/models`, pageInfo);
return response as any;
};
//获取模型提供者
@@ -135,16 +135,18 @@ interface UploadFileOptions {
kb_id?: string;
parent_id?: string;
onUploadProgress?: (event: AxiosProgressEvent) => void;
signal?: AbortSignal;
}
// 上传文件
export const uploadFile = async (data: FormData, options?: UploadFileOptions) => {
const { kb_id, parent_id, onUploadProgress } = options || {};
const { kb_id, parent_id, onUploadProgress, signal } = options || {};
const params: Record<string, string> = {};
if (kb_id) params.kb_id = kb_id;
if (parent_id) params.parent_id = parent_id;
const response = await request.uploadFile(`${apiPrefix}/files/file`, data, {
params,
onUploadProgress,
signal,
});
return response as UploadFileResponse;
};
@@ -199,8 +201,8 @@ export const deleteFile = async (id: string) => {
};
// 获取文档列表
export const getDocumentList = async (query: PathQuery) => {
const response = await request.get(`${apiPrefix}/documents/${query.kb_id}/${query.parent_id}/documents`, query);
export const getDocumentList = async (kb_id:string, query: PathQuery) => {
const response = await request.get(`${apiPrefix}/documents/${kb_id}/documents`, query);
return response as KnowledgeBaseDocumentData[];
};
// 文档详情
@@ -213,6 +215,11 @@ export const createDocument = async (data: KnowledgeBaseDocumentData) => {
const response = await request.post(`${apiPrefix}/documents/document`, data);
return response as KnowledgeBaseDocumentData;
};
// 自定义文档上传并创建
export const createDocumentAndUpload = async ( data: any, params: PathQuery) => {
const response = await request.post(`${apiPrefix}/files/customtext`, data, { params } );
return response as any;
};
// 更新文档
export const updateDocument = async (id: string, data: KnowledgeBaseDocumentData) => {
const response = await request.put(`${apiPrefix}/documents/${id}`, data);
@@ -223,9 +230,9 @@ export const deleteDocument = async (id: string) => {
const response = await request.delete(`${apiPrefix}/documents/${id}`);
return response;
};
// 文档解析
export const parseDocument = async (id: string) => {
const response = await request.post(`${apiPrefix}/documents/${id}/chunks`);
// 文档解析 / 分块
export const parseDocument = async (id: string, data: any) => {
const response = await request.post(`${apiPrefix}/documents/${id}/chunks`, data);
return response as any;
};
// 文档分块预览

View File

@@ -8,7 +8,15 @@ import type {
import type {
ConfigForm as ExtractionConfigForm
} from '@/views/MemoryExtractionEngine/types'
import type {
ConfigForm as EmotionConfig
} from '@/views/EmotionEngine/types'
import type {
ConfigForm as SelfReflectionEngineConfig
} from '@/views/SelfReflectionEngine/types'
import type { TestParams } from '@/views/MemoryConversation'
import type { EndUser } from '@/views/UserMemoryDetail/types'
import { handleSSE, type SSEMessage } from '@/utils/stream'
// 记忆对话
export const readService = (query: TestParams) => {
@@ -59,6 +67,7 @@ export const getTotalEndUsers = () => {
export const getUserProfile = (end_user_id: string) => {
return request.get(`/memory/analytics/user_profile`, { end_user_id })
}
// 用户记忆-记忆洞察
export const getMemoryInsightReport = (end_user_id: string) => {
return request.get(`/memory-storage/analytics/memory_insight/report`, { end_user_id })
@@ -67,9 +76,20 @@ export const getMemoryInsightReport = (end_user_id: string) => {
export const getUserSummary = (end_user_id: string) => {
return request.get(`/memory-storage/analytics/user_summary`, { end_user_id })
}
// 记忆分类
export const getNodeStatistics = (end_user_id: string) => {
return request.get(`/memory-storage/analytics/node_statistics`, { end_user_id })
}
// 基本信息
export const getEndUserProfile = (end_user_id: string) => {
return request.get(`/memory-storage/read_end_user/profile`, { end_user_id })
}
export const updatedEndUserProfile = (values: EndUser) => {
return request.post(`/memory-storage/updated_end_user/profile`, values)
}
// 用户记忆-关系网络
export const getMemorySearchEdges = (end_user_id: string) => {
return request.get(`/memory-storage/search/entity_graph`, { end_user_id })
return request.get(`/memory-storage/analytics/graph_data`, { end_user_id })
}
// 用户记忆-用户兴趣分布
export const getHotMemoryTagsByUser = (end_user_id: string) => {
@@ -95,6 +115,26 @@ export const getChunkInsight = (end_user_id: string) => {
export const getRagContent = (end_user_id: string) => {
return request.get(`/dashboard/rag_content`, { end_user_id, limit: 20 })
}
// 情感分布分析
export const getWordCloud = (group_id: string) => {
return request.post(`/memory/emotion/wordcloud`, { group_id, limit: 20 })
}
// 高频情绪关键词
export const getEmotionTags = (group_id: string) => {
return request.post(`/memory/emotion/tags`, { group_id, limit: 20 })
}
// 情绪健康指数
export const getEmotionHealth = (group_id: string) => {
return request.post(`/memory/emotion/health`, { group_id, limit: 20 })
}
// 个性化建议
export const getEmotionSuggestions = (group_id: string) => {
return request.post(`/memory/emotion/suggestions`, { group_id, limit: 20 })
}
export const analyticsRefresh = (end_user_id: string) => {
return request.post('/memory-storage/analytics/generate_cache', { end_user_id })
}
/*************** end 用户记忆 相关接口 ******************************/
/****************** 记忆管理 相关接口 *******************************/
@@ -132,9 +172,30 @@ export const updateMemoryExtractionConfig = (values: ExtractionConfigForm) => {
return request.post('/memory-storage/update_config_extracted', values)
}
// 记忆萃取引擎-试运行
export const pilotRunMemoryExtractionConfig = (values: { config_id: number | string; dialogue_text: string }) => {
return request.post('/memory-storage/pilot_run', values)
export const pilotRunMemoryExtractionConfig = (values: { config_id: number | string; dialogue_text: string; }, onMessage?: (data: SSEMessage[]) => void) => {
return handleSSE('/memory-storage/pilot_run', values, onMessage)
}
// 情绪引擎-获取配置
export const getMemoryEmotionConfig = (config_id: number | string) => {
return request.get('/memory/emotion/read_config', { config_id: config_id })
}
// 情绪引擎-更新配置
export const updateMemoryEmotionConfig = (values: EmotionConfig) => {
return request.post('/memory/emotion/updated_config', values)
}
// 反思引擎-获取配置
export const getMemoryReflectionConfig = (config_id: number | string) => {
return request.get('/memory/reflection/configs', { config_id: config_id })
}
// 反思引擎-更新配置
export const updateMemoryReflectionConfig = (values: SelfReflectionEngineConfig) => {
return request.post('/memory/reflection/save', values)
}
// 反思引擎-试运行
export const pilotRunMemoryReflectionConfig = (values: { config_id: number | string; language_type: string; }) => {
return request.get('/memory/reflection/run', values)
}
/*************** end 记忆管理 相关接口 ******************************/

17
web/src/api/order.ts Normal file
View File

@@ -0,0 +1,17 @@
import { request } from '@/utils/request'
import type { VoucherForm } from '@/views/OrderPayment/types'
export const getOrderListUrl = '/v1/orders/customer'
// 提交支付凭证API
export const submitPaymentVoucherAPI = (voucherData: VoucherForm) => {
return request.post('/v1/orders/', voucherData)
}
// 订单详情
export const getOrderDetail = (order_no: string) => {
return request.get(`/v1/orders/customer/${order_no}`)
}
export const orderStatusUrl = '/v1/order-status/'
export const getOrderStatus = () => {
return request.get(orderStatusUrl)
}

12
web/src/api/prompt.ts Normal file
View File

@@ -0,0 +1,12 @@
import { request } from '@/utils/request'
import type { AiPromptForm } from '@/views/ApplicationConfig/types'
export const createPromptSessions = () => {
return request.post(`/prompt/sessions`)
}
export const getPrompt = (session_id: string) => {
return request.get(`/prompt/sessions/${session_id}`)
}
export const updatePromptMessages = (session_id: string, data: AiPromptForm) => {
return request.post(`/prompt/sessions/${session_id}/messages`, data)
}

31
web/src/api/tools.ts Normal file
View File

@@ -0,0 +1,31 @@
import { request } from '@/utils/request'
import type { Query, CustomToolItem, ExecuteData, MCPToolItem, InnerToolItem } from '@/views/ToolManagement/types'
// 工具列表
export const getTools = (data: Query) => {
return request.get('/tools', data)
}
// 创建MCP工具
export const addTool = (values: MCPToolItem | CustomToolItem) => {
return request.post('/tools', values)
}
// 更新工具
export const updateTool = (tool_id: string, data: MCPToolItem | InnerToolItem | CustomToolItem) => {
return request.put(`/tools/${tool_id}`, data)
}
// 删除工具
export const deleteTool = (tool_id: string) => {
return request.delete(`/tools/${tool_id}`)
}
// MCP 测试连接
export const testConnection = (tool_id: string) => {
return request.post(`/tools/${tool_id}/test`)
}
// 工具测试
export const execute = (data: ExecuteData) => {
return request.post(`/tools/execution/execute`, data)
}
export const parseSchema = (data: Record<string, any>) => {
return request.post(`/tools/parse_schema`, data)
}

View File

@@ -1,5 +1,6 @@
import { request } from '@/utils/request'
import type { SpaceModalData } from '@/views/SpaceManagement/types'
import type { ConfigModalData } from '@/views/UserMemory/types'
// 空间列表
export const getWorkspaces = () => {
@@ -22,6 +23,6 @@ export const getWorkspaceModels = () => {
return request.get(`/workspaces/workspace_models`)
}
// 更新空间模型配置
export const updateWorkspaceModels = () => {
return request.post(`/workspaces/workspace_models`)
export const updateWorkspaceModels = (data: ConfigModalData) => {
return request.put(`/workspaces/workspace_models`, data)
}

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>全屏</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="红熊空间-记忆管理-个人记忆" transform="translate(-967, -349)" stroke="#5B6167">
<g id="图谱" transform="translate(432, 334)">
<g id="编组-18" transform="translate(535, 13)">
<g id="全屏" transform="translate(0, 2)">
<g id="编组-6" transform="translate(2, 2)">
<polyline id="路径" points="0 3 0 0 3 0"></polyline>
<polyline id="路径" transform="translate(10.5, 1.5) scale(-1, 1) translate(-10.5, -1.5)" points="9 3 9 0 12 0"></polyline>
<polyline id="路径" transform="translate(1.5, 10.5) scale(1, -1) translate(-1.5, -10.5)" points="0 12 0 9 3 9"></polyline>
<polyline id="路径" transform="translate(10.5, 10.5) scale(-1, -1) translate(-10.5, -10.5)" points="9 12 9 9 12 9"></polyline>
<line x1="5.30274338e-05" y1="0" x2="4.00960414" y2="4.02323899" id="路径-3"></line>
<line x1="12" y1="0" x2="8.01466597" y2="4.01783837" id="路径-4"></line>
<line x1="0" y1="11.9995998" x2="4.00871245" y2="8.01746225" id="路径-5"></line>
<line x1="11.9977093" y1="12" x2="8.01643138" y2="8.01746225" id="路径-7"></line>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>全屏</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="红熊空间-记忆管理-个人记忆" transform="translate(-967, -349)" stroke="#212332">
<g id="图谱" transform="translate(432, 334)">
<g id="编组-18" transform="translate(535, 13)">
<g id="全屏" transform="translate(0, 2)">
<g id="编组-6" transform="translate(2, 2)">
<polyline id="路径" points="0 3 0 0 3 0"></polyline>
<polyline id="路径" transform="translate(10.5, 1.5) scale(-1, 1) translate(-10.5, -1.5)" points="9 3 9 0 12 0"></polyline>
<polyline id="路径" transform="translate(1.5, 10.5) scale(1, -1) translate(-1.5, -10.5)" points="0 12 0 9 3 9"></polyline>
<polyline id="路径" transform="translate(10.5, 10.5) scale(-1, -1) translate(-10.5, -10.5)" points="9 12 9 9 12 9"></polyline>
<line x1="5.30274338e-05" y1="0" x2="4.00960414" y2="4.02323899" id="路径-3"></line>
<line x1="12" y1="0" x2="8.01466597" y2="4.01783837" id="路径-4"></line>
<line x1="0" y1="11.9995998" x2="4.00871245" y2="8.01746225" id="路径-5"></line>
<line x1="11.9977093" y1="12" x2="8.01643138" y2="8.01746225" id="路径-7"></line>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 16</title>
<g id="V1.0版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="红熊空间-记忆看板" transform="translate(-1372, -1136)" stroke="#212332">
<g id="快速操作" transform="translate(848, 1048)">
<g id="1" transform="translate(296, 68)">
<g id="编组-16" transform="translate(228, 20)">
<line x1="13.7142857" y1="2.28571429" x2="2.28571429" y2="13.7142857" id="路径-15"></line>
<polyline id="路径" points="5.55102041 2.28571429 13.7142857 2.28571429 13.7142857 10.4489796"></polyline>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 929 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>菜单-收费管理</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="首页-弹窗" transform="translate(-904, -514)" stroke="#5F6266">
<g id="编组-7" transform="translate(904, 314)">
<g id="菜单-收费管理" transform="translate(0, 200)">
<g id="编组-8" transform="translate(2, 2)">
<polyline id="路径" points="7.5 5.80029489 6 7.30029489 4.5 5.80029489"></polyline>
<path d="M9.5,2 L10.5,2 C11.3284271,2 12,2.67157288 12,3.5 L12,10.5 C12,11.3284271 11.3284271,12 10.5,12 L1.5,12 C0.671572875,12 0,11.3284271 0,10.5 L0,3.5 C0,2.67157288 0.671572875,2 1.5,2 L2.5,2 L2.5,2" id="路径"></path>
<path d="M2.5,2.5 L2.5,1 C2.5,0.44771525 2.94771525,1.11022302e-16 3.5,0 L8.5,0 C9.05228475,0 9.5,0.44771525 9.5,1 L9.5,2.5 L9.5,2.5" id="路径"></path>
<line x1="0" y1="4.01402447" x2="12" y2="4.01402447" id="路径-4"></line>
<line x1="4.01045351" y1="1.8" x2="8.01045351" y2="1.8" id="路径-5"></line>
<line x1="6" y1="7.22565966" x2="6" y2="10.0211794" id="路径-6"></line>
<line x1="4.5" y1="7.70544887" x2="7.5" y2="7.70544887" id="路径-7"></line>
<line x1="4.5" y1="9.20544887" x2="7.5" y2="9.20544887" id="路径-7"></line>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>菜单-收费管理</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="首页-弹窗" transform="translate(-975, -514)" stroke="#212332">
<g id="编组-7" transform="translate(975, 314)">
<g id="菜单-收费管理" transform="translate(0, 200)">
<g id="编组-8" transform="translate(2, 2)">
<polyline id="路径" points="7.5 5.80029489 6 7.30029489 4.5 5.80029489"></polyline>
<path d="M9.5,2 L10.5,2 C11.3284271,2 12,2.67157288 12,3.5 L12,10.5 C12,11.3284271 11.3284271,12 10.5,12 L1.5,12 C0.671572875,12 0,11.3284271 0,10.5 L0,3.5 C0,2.67157288 0.671572875,2 1.5,2 L2.5,2 L2.5,2" id="路径"></path>
<path d="M2.5,2.5 L2.5,1 C2.5,0.44771525 2.94771525,1.11022302e-16 3.5,0 L8.5,0 C9.05228475,0 9.5,0.44771525 9.5,1 L9.5,2.5 L9.5,2.5" id="路径"></path>
<line x1="0" y1="4.01402447" x2="12" y2="4.01402447" id="路径-4"></line>
<line x1="4.01045351" y1="1.8" x2="8.01045351" y2="1.8" id="路径-5"></line>
<line x1="6" y1="7.22565966" x2="6" y2="10.0211794" id="路径-6"></line>
<line x1="4.5" y1="7.70544887" x2="7.5" y2="7.70544887" id="路径-7"></line>
<line x1="4.5" y1="9.20544887" x2="7.5" y2="9.20544887" id="路径-7"></line>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 29</title>
<g id="V1.0版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="红熊空间-应用管理" transform="translate(-24, -249)" stroke="#212332">
<g id="记忆库" transform="translate(12, 241)">
<g id="编组-29" transform="translate(12, 8)">
<g id="编组-30" transform="translate(1.5, 2)">
<path d="M5.15208739,12 L3.96504286,11.9873871 C3.14287934,11.9786512 2.48098017,11.3096817 2.48098017,10.4874718 L2.48098017,8.17573134 L2.48098017,8.17573134 L0.701703053,7.96314675 C0.482349244,7.93693878 0.325773604,7.73787163 0.351981572,7.51851782 C0.360586526,7.44649662 0.388612869,7.37817062 0.433061291,7.3208519 L1.79815052,5.56049306 L1.79815052,5.56049306 L1.79815052,5.43150782 C1.79815052,2.43176888 4.30576994,0 7.39907526,0 C10.4923806,0 13,2.43176888 13,5.43150782" id="路径" stroke-linecap="round"></path>
<path d="M8,6 L11,6 C11.5522847,6 12,6.44771525 12,7 L12,11 C12,11.5522847 11.5522847,12 11,12 L8,12 C7.44771525,12 7,11.5522847 7,11 L7,7 C7,6.44771525 7.44771525,6 8,6 Z" id="矩形"></path>
<line x1="7" y1="8" x2="12" y2="8" id="路径-26"></line>
<line x1="7" y1="10" x2="12" y2="10" id="路径-26备份"></line>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>注意</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="收费管理" transform="translate(-280, -839)" fill="#FF5D34" fill-rule="nonzero">
<g id="编组-10" transform="translate(264, 823)">
<g id="注意" transform="translate(16, 16)">
<path d="M8.7755948,1.95407174 C9.31341765,1.01530942 10.6865824,1.01530942 11.2358482,1.95407174 L19.8066846,16.6349572 C20.3559505,17.5737195 19.6693682,18.7386896 18.5708364,18.75 L1.42916361,18.75 C0.330631841,18.75 -0.355950514,17.5737195 0.19331537,16.6349572 Z M10,15 C9.48223305,15 9.0625,15.419733 9.0625,15.9375 C9.0625,16.455267 9.48223305,16.875 10,16.875 C10.517767,16.875 10.9375,16.455267 10.9375,15.9375 C10.9375,15.419733 10.517767,15 10,15 Z M10,6.25 C9.48223305,6.25 9.0625,6.66973305 9.0625,7.1875 L9.0625,12.8125 C9.0625,13.330267 9.48223305,13.75 10,13.75 C10.517767,13.75 10.9375,13.330267 10.9375,12.8125 L10.9375,7.1875 C10.9375,6.66973305 10.517767,6.25 10,6.25 Z" id="形状结合"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>对号备份</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="定价-支付" transform="translate(-941, -357)" fill="#5B6167" fill-rule="nonzero">
<g id="编组-8" transform="translate(360, 219)">
<g id="编组-2" transform="translate(581, 134)">
<g id="对号备份" transform="translate(0, 4)">
<path d="M6,0 C10,0 12,2 12,6 C12,10 10,12 6,12 C2,12 0,10 0,6 C0,2 2,0 6,0 Z M8.96637129,3.91456632 C8.74659858,3.69499924 8.39044332,3.69516594 8.17087624,3.91493864 L5.06113614,7.0275 L3.85141273,5.81979618 C3.63162085,5.60024831 3.2754656,5.60044612 3.05591773,5.82023801 C2.83636985,6.04002989 2.83656766,6.39618514 3.05635955,6.61573301 L4.6637238,8.22131277 C4.88350211,8.44084708 5.23963056,8.44066484 5.45918407,8.22090572 L8.9667436,4.71006136 C9.18631068,4.49028866 9.18614399,4.1341334 8.96637129,3.91456632 Z" id="形状结合"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>企业_画板 39@2x</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="定价-支付" transform="translate(-404, -623)" fill="#5B6167" fill-rule="nonzero">
<g id="编组-9" transform="translate(360, 572)">
<g id="编组-13" transform="translate(24, 28)">
<g id="企业_画板-39" transform="translate(20, 23)">
<path d="M22.6288053,2.37305419 C23.0204473,2.23747924 23.3145868,2.25900923 23.6919502,2.44276018 C24.0617664,2.62288728 25.6534241,3.72773786 25.9496034,4.01380953 C26.0658721,4.12614915 26.3712305,4.58595288 26.3712305,4.72515169 L26.3712305,28.0018359 C26.4530266,28.0862505 27.1973505,27.7436893 27.3764451,28.1371977 C27.4557934,28.3119956 27.4672163,29.6287353 27.2467137,29.6287353 L21.8000336,29.6287353 C21.7818794,29.6287353 21.6233867,29.5236434 21.6056404,29.4931604 L21.5446504,5.02657147 C21.4958991,4.86989287 21.331899,4.74412365 21.166879,4.77098284 C21.0187893,4.79507085 20.3731919,5.12974486 20.1677838,5.2218335 C18.029459,6.17981122 15.6408506,7.2130373 13.5747348,8.29294721 C13.376262,8.3967601 13.2344958,8.39505476 13.2049187,8.65192239 C13.1657544,8.99022026 13.1547395,10.3811425 13.2102221,10.6870388 C13.2300082,10.7957546 13.3515804,10.9596809 13.449083,10.9831294 C13.6673418,11.0353556 15.7193829,10.1487892 16.1403981,10.0260044 C16.2782888,9.98571558 16.4594232,9.91110672 16.5977218,9.92389681 C16.9036922,9.95203501 18.7858177,11.1775388 19.1684847,11.4395224 C19.2908728,11.5232975 19.6443705,11.7298575 19.680475,11.8532818 L19.7406492,27.6469109 C19.9835897,28.1139624 20.7572867,27.6294311 20.9818689,28.04383 C21.1122123,28.2842837 21.1334262,29.458414 20.8600927,29.5944153 L15.2826613,29.5950548 L15.1829149,29.4283573 L15.1563975,12.6505308 C15.1329398,12.3595562 15.1168253,12.2231286 14.7978003,12.2116175 L7.01432266,14.8538369 L6.69162593,15.0586915 L6.6579692,29.3893475 C6.65144183,29.4910287 6.60044677,29.537073 6.52640195,29.5907914 C6.29610827,29.7579152 4.81786354,29.7711317 4.64346045,29.5325965 C4.54534596,29.3987269 4.56003253,28.3989682 4.61306739,28.1992296 C4.68996794,27.9097473 5.17544089,28.12995 5.23622699,27.6326287 L5.23459515,14.2139061 L5.32414247,14.0365502 L11.5253454,11.7079012 C11.6140768,11.6079253 11.6389624,11.4994227 11.653445,11.3674716 C11.7713456,10.2926777 11.5033156,8.89003123 11.6560968,7.84657308 C11.6811863,7.67582538 11.7862362,7.47629998 11.9263706,7.38357183 Z M13.1298539,26.7341249 C13.41767,26.7230401 13.7566852,26.6450206 13.8427648,27.033413 C13.8603581,27.1127115 13.87035,27.429576 13.8728157,27.8021561 L13.8727754,28.1865728 C13.8693843,28.7059304 13.8526579,29.2165747 13.8227748,29.2874531 C13.7956454,29.3518299 13.7146652,29.411517 13.6606105,29.4569218 C12.4008286,29.6148794 11.1332954,29.5374994 9.86617025,29.557324 C9.31134403,29.5660639 8.69348791,29.6911936 8.11398008,29.632146 C7.95915908,29.6163716 7.80291023,29.5944153 7.73049725,29.423028 C7.65910416,29.2537725 7.6701191,27.3030707 7.7443679,27.1365863 C7.77170125,27.0754071 7.80291023,27.0187044 7.8692038,26.9961085 C9.62119,26.9070042 11.3768478,26.8021255 13.1298539,26.734338 Z M13.6149189,22.525759 C13.8127797,22.624669 13.840929,22.7357297 13.8605111,22.949111 C13.902531,23.408062 13.902939,24.395457 13.8564315,24.85185 C13.8466405,24.9490547 13.8360335,25.1031753 13.748322,25.1609438 L7.69643255,26.0366386 C7.79169131,25.3491713 7.56833296,24.2673429 7.69643255,23.6310359 C7.70561166,23.5860575 7.77272115,23.5152856 7.79413908,23.4619936 Z M13.5565806,18.2619694 C13.7026304,18.3067348 13.8248146,18.3877387 13.8552076,18.5499596 C13.9153818,18.8720567 13.8951878,20.217361 13.8603071,20.5855024 C13.851128,20.6816413 13.8684663,20.7790591 13.7964613,20.8577182 L13.5476054,21.0041647 L7.69663653,22.3096065 L7.69663653,20.0395787 C7.69663653,20.0163434 7.79638286,19.8671257 7.8290197,19.8392007 Z M13.5115009,13.9915717 C13.6646901,13.9862425 13.8266504,14.1254413 13.8554116,14.2808409 C13.9096703,14.5739471 13.9006952,16.0603687 13.8598992,16.3835316 C13.8480683,16.4779651 13.8456206,16.5600348 13.7634165,16.6216404 L7.99179592,18.5844929 C7.73090521,18.5465489 7.71581067,18.222107 7.70030817,18.0093652 C7.671139,17.60733 7.60851707,16.2677813 7.89164163,16.041823 Z" id="形状结合"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>订单</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="收费管理" transform="translate(-1266, -130)" fill-rule="nonzero">
<g id="编组-5" transform="translate(264, 88)">
<g id="编组-9" transform="translate(986, 32)">
<g id="订单" transform="translate(16, 10)">
<rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="16" height="16"></rect>
<path d="M12.8333281,4 C12.8333281,3.72385763 13.0571858,3.5 13.3333281,3.5 C13.6094705,3.5 13.8333281,3.72385763 13.8333281,4 L13.8333281,12.8333281 C13.8333281,13.8458281 13.0125,14.6666719 12,14.6666719 L4,14.6666719 C2.9875,14.6666719 2.16667188,13.8458281 2.16667188,12.8333281 L2.16667188,3.16667188 C2.16667188,2.15417187 2.9875,1.33332812 4,1.33332812 L13.3333281,1.33332812 C13.6094705,1.33332812 13.8333281,1.55718575 13.8333281,1.83332812 C13.8333281,2.1094705 13.6094705,2.33332812 13.3333281,2.33332812 L4,2.33332812 C3.77898493,2.33332812 3.5670223,2.42112709 3.41074251,2.57740981 C3.25446272,2.73369252 3.16667188,2.94565681 3.16667188,3.16667188 L3.16667188,12.8333281 C3.16667188,13.0543432 3.25446272,13.2663075 3.41074251,13.4225902 C3.5670223,13.5788729 3.77898493,13.6666719 4,13.6666719 L12,13.6666719 C12.2210151,13.6666719 12.4329777,13.5788729 12.5892575,13.4225902 C12.7455373,13.2663075 12.8333323,13.0543432 12.8333281,12.8333281 L12.8333281,4 Z M5.33332812,6.83332812 C5.05718575,6.83332812 4.83332812,6.6094705 4.83332812,6.33332812 C4.83332812,6.05718575 5.05718575,5.83332812 5.33332812,5.83332812 L10.6666719,5.83332812 C10.9428142,5.83332812 11.1666719,6.05718575 11.1666719,6.33332812 C11.1666719,6.6094705 10.9428142,6.83332812 10.6666719,6.83332812 L5.33332812,6.83332812 Z M5.33332812,9.5 C5.05718575,9.5 4.83332812,9.27614237 4.83332812,9 C4.83332812,8.72385763 5.05718575,8.5 5.33332812,8.5 L8.66667188,8.5 C8.94281425,8.5 9.16667188,8.72385763 9.16667188,9 C9.16667188,9.27614237 8.94281425,9.5 8.66667188,9.5 L5.33332812,9.5 Z" id="形状" stroke="#212332" stroke-width="0.4" fill="#212332"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>订单</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="收费管理" transform="translate(-1266, -130)" fill-rule="nonzero">
<g id="编组-5" transform="translate(264, 88)">
<g id="编组-9" transform="translate(986, 32)">
<g id="订单" transform="translate(16, 10)">
<rect id="矩形" fill="#155EEF" opacity="0" x="0" y="0" width="16" height="16"></rect>
<path d="M12.8333281,4 C12.8333281,3.72385763 13.0571858,3.5 13.3333281,3.5 C13.6094705,3.5 13.8333281,3.72385763 13.8333281,4 L13.8333281,12.8333281 C13.8333281,13.8458281 13.0125,14.6666719 12,14.6666719 L4,14.6666719 C2.9875,14.6666719 2.16667188,13.8458281 2.16667188,12.8333281 L2.16667188,3.16667188 C2.16667188,2.15417187 2.9875,1.33332812 4,1.33332812 L13.3333281,1.33332812 C13.6094705,1.33332812 13.8333281,1.55718575 13.8333281,1.83332812 C13.8333281,2.1094705 13.6094705,2.33332812 13.3333281,2.33332812 L4,2.33332812 C3.77898493,2.33332812 3.5670223,2.42112709 3.41074251,2.57740981 C3.25446272,2.73369252 3.16667188,2.94565681 3.16667188,3.16667188 L3.16667188,12.8333281 C3.16667188,13.0543432 3.25446272,13.2663075 3.41074251,13.4225902 C3.5670223,13.5788729 3.77898493,13.6666719 4,13.6666719 L12,13.6666719 C12.2210151,13.6666719 12.4329777,13.5788729 12.5892575,13.4225902 C12.7455373,13.2663075 12.8333323,13.0543432 12.8333281,12.8333281 L12.8333281,4 Z M5.33332812,6.83332812 C5.05718575,6.83332812 4.83332812,6.6094705 4.83332812,6.33332812 C4.83332812,6.05718575 5.05718575,5.83332812 5.33332812,5.83332812 L10.6666719,5.83332812 C10.9428142,5.83332812 11.1666719,6.05718575 11.1666719,6.33332812 C11.1666719,6.6094705 10.9428142,6.83332812 10.6666719,6.83332812 L5.33332812,6.83332812 Z M5.33332812,9.5 C5.05718575,9.5 4.83332812,9.27614237 4.83332812,9 C4.83332812,8.72385763 5.05718575,8.5 5.33332812,8.5 L8.66667188,8.5 C8.94281425,8.5 9.16667188,8.72385763 9.16667188,9 C9.16667188,9.27614237 8.94281425,9.5 8.66667188,9.5 L5.33332812,9.5 Z" id="形状" stroke="#155EEF" stroke-width="0.4" fill="#155EEF"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -2,7 +2,7 @@
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>刷新</title>
<g id="V1.0版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="应用管理--API" transform="translate(-1071, -490)" fill="#212332" fill-rule="nonzero">
<g id="应用管理--API" transform="translate(-1071, -490)" fill="#5B6167" fill-rule="nonzero">
<g id="2" transform="translate(220, 336)">
<g id="主操作" transform="translate(839, 144)">
<g id="刷新" transform="translate(12, 10)">

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>刷新</title>
<g id="V1.0版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="应用管理--API" transform="translate(-1071, -490)" fill="#155EEF" fill-rule="nonzero">
<g id="2" transform="translate(220, 336)">
<g id="主操作" transform="translate(839, 144)">
<g id="刷新" transform="translate(12, 10)">
<path d="M14.5,6.60760714 L14.5,3.35760714 L13.4384813,4.41757143 C12.2346397,2.59629629 10.195406,1.50029795 8.00999678,1.5 C4.41487535,1.5 1.5,4.41014286 1.5,8.00046429 C1.5,11.5907857 4.41487535,14.5009287 8.00999678,14.5009287 C10.6602392,14.5014558 13.046297,12.8977788 14.0434028,10.4458571 C14.1184045,10.261489 14.0892053,10.0511714 13.9668045,9.89412914 C13.8444036,9.73708685 13.6473966,9.65717825 13.4499941,9.68450414 C13.2525917,9.71183003 13.0847838,9.84223896 13.0097822,10.0266071 C12.1832078,12.0581943 10.2060581,13.38691 8.00999678,13.3866429 C5.03095604,13.3866429 2.61591974,10.9751429 2.61591974,8.00046429 C2.61591974,5.02578571 5.03095604,2.61428571 8.00999678,2.61428571 C9.93449337,2.61428571 11.6706785,3.63107143 12.6308344,5.22357143 L11.2452341,6.60760714 L14.5,6.60760714 Z" id="路径"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 775 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 792 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 908 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 979 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 869 B

View File

@@ -34,12 +34,12 @@ const ButtonCheckbox: FC<ButtonCheckboxProps> = ({
}
return (
<div className={clsx("rb:flex rb:items-center rb:border rb:rounded-[8px] rb:px-[8px] rb:text-[12px] rb:h-[24px] rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
<div className={clsx("rb:flex rb:items-center rb:border rb:rounded-lg rb:px-2 rb:text-[12px] rb:h-6 rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
"rb:bg-[rgba(21,94,239,0.06)] rb:border-[#155EEF] rb:text-[#155EEF]": checked,
"rb:border-[#DFE4ED] rb:text-[#212332]": !checked,
})} onClick={handleChange}>
{icon && !checked && <img src={icon} className="rb:w-[16px] rb:h-[16px] rb:mr-[4px]" />}
{checkedIcon && checked && <img src={checkedIcon} className="rb:w-[16px] rb:h-[16px] rb:mr-[4px]" />}
{icon && !checked && <img src={icon} className="rb:w-4 rb:h-4 rb:mr-1" />}
{checkedIcon && checked && <img src={checkedIcon} className="rb:w-4 rb:h-4 rb:mr-1" />}
{children}
</div>
);

View File

@@ -0,0 +1,84 @@
/*
* @Author: ZhaoYing
* @Date: 2025-12-10 16:46:17
* @Last Modified by: ZhaoYing
* @Last Modified time: 2025-12-11 13:40:18
*/
import { type FC, useRef, useEffect } from 'react'
import clsx from 'clsx'
import Markdown from '@/components/Markdown'
import type { ChatContentProps } from './types'
/**
* 聊天内容显示组件
* 负责渲染聊天消息列表,支持不同角色的消息样式和自动滚动
*/
const ChatContent: FC<ChatContentProps> = ({
classNames,
contentClassNames,
data = [],
streamLoading = false,
empty,
labelPosition = 'bottom',
labelFormat,
errorDesc
}) => {
// 滚动容器引用,用于控制自动滚动到底部
const scrollContainerRef = useRef<(HTMLDivElement | null)>(null)
// 当数据变化时,自动滚动到底部显示最新消息
useEffect(() => {
setTimeout(() => {
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight;
}
}, 0);
}, [data])
return (
<div ref={scrollContainerRef} className={clsx("rb:relative rb:overflow-y-auto", classNames)}>
{data.length === 0
? empty // 显示空状态
: data.map((item, index) => (
<div key={index} className={clsx("rb:relative", {
'rb:mt-6': index !== 0, // 非第一条消息添加上边距
'rb:right-0 rb:text-right': item.role === 'user', // 用户消息右对齐
'rb:left-0 rb:text-left': item.role === 'assistant', // 助手消息左对齐
})}>
{/* 流式加载时且内容为空则不显示 */}
{streamLoading && item.content === ''
? null
: <>
{/* 顶部标签(如时间戳、用户名等) */}
{labelPosition === 'top' &&
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4 rb:font-regular">
{labelFormat(item)}
</div>
}
{/* 消息气泡框 */}
<div className={clsx('rb:border rb:text-left rb:rounded-lg rb:mt-1.5 rb:leading-4.5 rb:p-[10px_12px_2px_12px] rb:inline-block rb:max-w-100', contentClassNames, {
// 错误消息样式内容为null且非助手消息
'rb:border-[rgba(255,93,52,0.30)] rb:bg-[rgba(255,93,52,0.08)] rb:text-[#FF5D34]': errorDesc && item.role === 'assistant' && item.content === null,
// 助手消息样式
'rb:bg-[rgba(21,94,239,0.08)] rb:border-[rgba(21,94,239,0.30)]': item.role === 'user',
// 用户消息样式
'rb:bg-[#FFFFFF] rb:border-[#EBEBEB]': item.role === 'assistant' && (item.content || item.content === ''),
})}>
{/* 使用Markdown组件渲染消息内容 */}
<Markdown content={item.content ?? errorDesc ?? ''} />
</div>
{/* 底部标签(如时间戳、用户名等) */}
{labelPosition === 'bottom' &&
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4 rb:font-regular">
{labelFormat(item)}
</div>
}
</>
}
</div>
))
}
</div>
)
}
export default ChatContent

View File

@@ -0,0 +1,80 @@
/*
* @Author: ZhaoYing
* @Date: 2025-12-10 16:46:14
* @Last Modified by: ZhaoYing
* @Last Modified time: 2025-12-20 15:38:40
*/
import { useEffect } from 'react'
import { Flex, Input, Form } from 'antd'
import SendIcon from '@/assets/images/conversation/send.svg'
import SendDisabledIcon from '@/assets/images/conversation/sendDisabled.svg'
import LoadingIcon from '@/assets/images/conversation/loading.svg'
import type { ChatInputProps } from './types'
/**
* 聊天输入框组件
* 提供消息输入、发送功能,支持键盘快捷键和加载状态显示
*/
const ChatInput = ({ message, onChange, onSend, loading, children }: ChatInputProps) => {
const [form] = Form.useForm()
// 监听表单值变化,用于控制发送按钮状态
const values = Form.useWatch([], form);
// 当外部message为空时清空表单
useEffect(() => {
if (!message) {
form.setFieldsValue({
message: undefined,
})
}
}, [form, message])
// 当加载状态时,清空输入框
useEffect(() => {
if (loading) {
form.setFieldsValue({
message: undefined,
})
}
}, [loading])
return (
<div className="rb:absolute rb:bottom-3 rb:left-0 rb:right-0">
<Flex vertical justify="space-between" className="rb:border rb:border-[#DFE4ED] rb:rounded-xl rb:min-h-30">
{/* 消息输入表单 */}
<Form form={form} layout="vertical">
<Form.Item name="message" noStyle>
<Input.TextArea
className="rb:m-[10px_12px_10px_12px]! rb:p-0! rb:w-[calc(100%-24px)]! rb:flex-[1_1_auto]"
variant="borderless"
autoSize={{ minRows: 2, maxRows: 2 }}
onChange={(e) => onChange(e.target.value)}
onKeyDown={(e) => {
// Enter键发送Shift+Enter换行
if (e.key === 'Enter' && !e.shiftKey && (e.target as HTMLTextAreaElement).value?.trim() !== '' && !loading) {
e.preventDefault();
onSend();
}
}}
/>
</Form.Item>
</Form>
{/* 底部操作区域 */}
<Flex align="center" justify="space-between" className="rb:m-[0_10px_10px_10px]!">
{/* 子组件内容(如按钮等) */}
{children}
{/* 发送按钮 - 根据状态显示不同图标 */}
{loading
? <img src={LoadingIcon} className="rb:w-5.5 rb:h-5.5 rb:cursor-pointer" />
: !values || !values?.message || values?.message?.trim() === ''
? <img src={SendDisabledIcon} className="rb:w-5.5 rb:h-5.5 rb:cursor-pointer" />
: <img src={SendIcon} className="rb:w-5.5 rb:h-5.5 rb:cursor-pointer" onClick={onSend} />
}
</Flex>
</Flex>
</div>
)
}
export default ChatInput

View File

@@ -0,0 +1,47 @@
/*
* @Author: ZhaoYing
* @Date: 2025-12-10 16:46:09
* @Last Modified by: ZhaoYing
* @Last Modified time: 2025-12-11 13:43:51
*/
import { type FC } from 'react'
import ChatInput from './ChatInput'
import type { ChatProps } from './types'
import ChatContent from './ChatContent'
/**
* 聊天组件 - 主要组件,由内容区域和输入框组成
* 提供完整的聊天界面功能,包括消息显示和输入交互
*/
const Chat: FC<ChatProps> = ({
empty,
data,
onChange,
onSend,
streamLoading = false,
loading,
contentClassName = '',
children,
labelFormat,
errorDesc
}) => {
return (
<div className="rb:h-full rb:relative rb:pt-2">
{/* 聊天内容显示区域 */}
<ChatContent
classNames={contentClassName}
data={data}
streamLoading={streamLoading}
empty={empty}
labelFormat={labelFormat}
errorDesc={errorDesc}
/>
{/* 聊天输入框区域 */}
<ChatInput onChange={onChange} onSend={onSend} loading={loading}>
{children}
</ChatInput>
</div>
)
}
export default Chat

View File

@@ -0,0 +1,84 @@
/*
* @Author: ZhaoYing
* @Date: 2025-12-10 16:45:54
* @Last Modified by: ZhaoYing
* @Last Modified time: 2025-12-11 13:43:52
*/
import { type ReactNode } from 'react'
/**
* 聊天消息项接口
*/
export interface ChatItem {
/** 消息唯一标识 */
id?: string;
/** 会话ID */
conversation_id?: string | null;
/** 消息角色:用户或助手 */
role?: 'user' | 'assistant';
/** 消息内容 */
content?: string | null;
/** 创建时间 */
created_at?: number | string
}
/**
* 聊天组件主要属性接口
*/
export interface ChatProps {
/** 空状态显示内容 */
empty?: ReactNode;
/** 聊天数据列表 */
data: ChatItem[];
/** 输入内容变化回调 */
onChange: (message: string) => void;
/** 发送消息回调 */
onSend: () => void;
/** 流式加载状态 */
streamLoading?: boolean;
/** 加载状态 */
loading: boolean;
/** 内容区域自定义样式类名 */
contentClassName?: string;
/** 子组件内容 */
children?: ReactNode;
/** 标签格式化函数 */
labelFormat: (item: ChatItem) => any;
errorDesc?: string;
}
/**
* 聊天输入框组件属性接口
*/
export interface ChatInputProps {
/** 当前输入消息 */
message?: string;
/** 输入内容变化回调 */
onChange: (message: string) => void;
/** 发送消息回调 */
onSend: () => void;
/** 加载状态 */
loading: boolean;
/** 子组件内容 */
children?: ReactNode;
}
/**
* 聊天内容区域组件属性接口
*/
export interface ChatContentProps {
/** 自定义样式类名 */
classNames?: string | Record<string, boolean>;
contentClassNames?: string | Record<string, boolean>;
/** 聊天数据列表 */
data: ChatItem[];
/** 流式加载状态 */
streamLoading: boolean;
/** 空状态显示内容 */
empty?: ReactNode;
/** 标签位置:顶部或底部 */
labelPosition?: 'top' | 'bottom';
/** 标签格式化函数 */
labelFormat: (item: ChatItem) => any;
errorDesc?: string;
}

View File

@@ -9,7 +9,7 @@ interface ApiResponse<T> {
items?: T[];
}
interface CustomSelectProps {
interface CustomSelectProps extends Omit<SelectProps, 'filterOption'> {
url: string;
params?: Record<string, unknown>;
valueKey?: string;

View File

@@ -27,7 +27,7 @@ const Empty: FC<EmptyProps> = ({
<div className={`rb:flex rb:items-center rb:justify-center rb:flex-col ${className}`}>
<img src={url || emptyIcon} alt="404" style={{ width: `${width}px`, height: `${height}px` }} />
{title && <div className="rb:mt-2 rb:leading-5">{title}</div>}
{curSubTitle && <div className={`rb:mt-[${url ? 8 : 5}px] rb:leading-4 rb:text-[12px] rb:text-[#A8A9AA]`}>{subTitle}</div>}
{curSubTitle && <div className={`rb:mt-[${url ? 8 : 5}px] rb:leading-4 rb:text-[12px] rb:text-[#A8A9AA]`}>{curSubTitle}</div>}
</div>
);
}

View File

@@ -3,6 +3,7 @@ import { Layout, Dropdown, Space, Breadcrumb } from 'antd';
import type { MenuProps, BreadcrumbProps } from 'antd';
import { UserOutlined, LogoutOutlined, SettingOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { useUser } from '@/store/user';
import { useMenu } from '@/store/menu';
import styles from './index.module.css'
@@ -12,12 +13,35 @@ const { Header } = Layout;
const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
const { t } = useTranslation();
const location = useLocation();
const settingModalRef = useRef<SettingModalRef>(null)
const userInfoModalRef = useRef<UserInfoModalRef>(null)
const { user, logout } = useUser();
const { allBreadcrumbs } = useMenu();
const breadcrumbs = allBreadcrumbs[source] || [];
// 根据当前路由动态选择面包屑源
const getBreadcrumbSource = () => {
const pathname = location.pathname;
// 知识库列表页面使用默认的 space 面包屑
if (pathname === '/knowledge-base') {
return 'space';
}
// 知识库详情相关页面使用独立的面包屑
if (pathname.includes('/knowledge-base/') && pathname !== '/knowledge-base') {
return 'space-detail';
}
// 其他页面使用传入的 source
return source;
};
const breadcrumbSource = getBreadcrumbSource();
const breadcrumbs = allBreadcrumbs[breadcrumbSource] || [];
// 处理退出登录
const handleLogout = () => {

View File

@@ -13,7 +13,7 @@ const { Content } = Layout;
// 认证布局组件使用useRouteGuard hook进行路由鉴权
const AuthLayout: FC = () => {
const { getUserInfo } = useUser();
const { getUserInfo, getStorageType } = useUser();
// 使用路由守卫hook处理认证和权限检查
useRouteGuard('manage');
// 自动更新面包屑导航
@@ -24,6 +24,7 @@ const AuthLayout: FC = () => {
window.location.href = `/#/login`;
} else {
getUserInfo()
getStorageType()
}
}, []);

View File

@@ -4,12 +4,13 @@ import { useUser } from '@/store/user';
// 基础布局组件,用于展示内容并保留用户信息获取功能
const BasicLayout: FC = () => {
const { getUserInfo } = useUser();
const { getUserInfo, getStorageType } = useUser();
// 获取用户信息
useEffect(() => {
getUserInfo();
}, [getUserInfo]);
getStorageType()
}, [getUserInfo, getStorageType]);
return (
<div className="rb:relative rb:h-full rb:w-full">

View File

@@ -1,4 +1,5 @@
import { Image, Input, Select, Form, Checkbox, Radio, ColorPicker, DatePicker, TimePicker, InputNumber, Slider } from 'antd'
import { Image, Input, Select, Form, Checkbox, Radio, ColorPicker, DatePicker, TimePicker, InputNumber, Slider, Button } from 'antd'
import { EditOutlined, SaveOutlined, CloseOutlined } from '@ant-design/icons'
import ReactMarkdown from 'react-markdown'
import RemarkGfm from 'remark-gfm'
import RemarkMath from 'remark-math'
@@ -6,6 +7,7 @@ import RemarkBreaks from 'remark-breaks'
import RehypeKatex from 'rehype-katex'
import RehypeRaw from 'rehype-raw'
import type { FC } from 'react'
import { useState, useRef, useEffect } from 'react'
import Code from './Code'
import VideoBlock from './VideoBlock'
@@ -16,42 +18,45 @@ import RbButton from './RbButton'
interface RbMarkdownProps {
content: string;
showHtmlComments?: boolean; // 是否显示 HTML 注释,默认为 false隐藏
editable?: boolean; // 是否可编辑,默认为 false
onContentChange?: (content: string) => void; // 内容变化回调
onSave?: (content: string) => void; // 保存回调
}
const components = {
h1: ({ children }: { children: string }) => <h1 className="rb:text-2xl rb:font-bold rb:mb-2">{children}</h1>,
h2: ({ children }: { children: string }) => <h2 className="rb:text-xl rb:font-bold rb:mb-2">{children}</h2>,
h3: ({ children }: { children: string }) => <h3 className="rb:text-lg rb:font-bold rb:mb-2">{children}</h3>,
h4: ({ children }: { children: string }) => <h4 className="rb:text-md rb:font-bold rb:mb-2">{children}</h4>,
h5: ({ children }: { children: string }) => <h5 className="rb:text-sm rb:font-bold rb:mb-2">{children}</h5>,
h6: ({ children }: { children: string }) => <h6 className="rb:text-xs rb:font-bold rb:mb-2">{children}</h6>,
ul: ({ children }: { children: string }) => <ul className="rb:list-disc rb:ml-6 rb:mb-2">{children}</ul>,
ol: ({ children }: { children: string }) => <ol className="rb:list-decimal rb:ml-6 rb:mb-2">{children}</ol>,
li: ({ children }: { children: string }) => <li className="rb:mb-1">{children}</li>,
blockquote: ({ children }: { children: string }) => <blockquote className="rb:border-l-4 rb:border-[#D9D9D9] rb:pl-4 rb:mb-2">{children}</blockquote>,
p: ({ children }: { children: string }) => <p className="rb:mb-2">{children}</p>,
strong: ({ children }: { children: string }) => <strong className="rb:font-bold">{children}</strong>,
em: ({ children }: { children: string }) => <em className="rb:italic">{children}</em>,
del: ({ children }: { children: string }) => <del className="rb:line-through">{children}</del>,
span: ({ children, ...props }: any) => {
h1: ({ children, ...props }: any) => <h1 className="rb:text-2xl rb:font-bold rb:mb-2" {...props}>{children}</h1>,
h2: ({ children, ...props }: any) => <h2 className="rb:text-xl rb:font-bold rb:mb-2" {...props}>{children}</h2>,
h3: ({ children, ...props }: any) => <h3 className="rb:text-lg rb:font-bold rb:mb-2" {...props}>{children}</h3>,
h4: ({ children, ...props }: any) => <h4 className="rb:text-md rb:font-bold rb:mb-2" {...props}>{children}</h4>,
h5: ({ children, ...props }: any) => <h5 className="rb:text-sm rb:font-bold rb:mb-2" {...props}>{children}</h5>,
h6: ({ children, ...props }: any) => <h6 className="rb:text-xs rb:font-bold rb:mb-2" {...props}>{children}</h6>,
ul: ({ children, ...props }: any) => <ul className="rb:list-disc rb:ml-6 rb:mb-2" {...props}>{children}</ul>,
ol: ({ children, ...props }: any) => <ol className="rb:list-decimal rb:ml-6 rb:mb-2" {...props}>{children}</ol>,
li: ({ children, ...props }: any) => <li className="rb:mb-1" {...props}>{children}</li>,
blockquote: ({ children, ...props }: any) => <blockquote className="rb:border-l-4 rb:border-[#D9D9D9] rb:pl-4 rb:mb-2" {...props}>{children}</blockquote>,
p: ({ children, ...props }: any) => <p className="rb:mb-2" {...props}>{children}</p>,
strong: ({ children, ...props }: any) => <strong className="rb:font-bold" {...props}>{children}</strong>,
em: ({ children, ...props }: any) => <em className="rb:italic" {...props}>{children}</em>,
del: ({ children, ...props }: any) => <del className="rb:line-through" {...props}>{children}</del>,
span: ({ children, style, ...restProps }: any) => {
// 如果是 HTML 注释的 span应用特殊样式
if (props.style?.color === '#999') {
if (style?.color === '#999') {
return <span style={{ color: '#999', fontSize: '0.9em' }}>{children}</span>
}
return <span {...props}>{children}</span>
return <span style={style} {...restProps}>{children}</span>
},
code: Code,
img: Image,
video: VideoBlock,
audio: AudioBlock,
a: Link,
button: RbButton,
table: ({ children }: { children: string }) => <table className="rb:border rb:border-[#D9D9D9] rb:mb-2">{children}</table>,
tr: ({ children }: { children: string }) => <tr className="rb:border rb:border-[#D9D9D9]">{children}</tr>,
th: ({ children }: { children: string }) => <th className="rb:border rb:border-[#D9D9D9] rb:px-2 rb:py-1 rb:text-left rb:font-bold">{children}</th>,
td: ({ children }: { children: string }) => <td className="rb:border rb:border-[#D9D9D9] rb:px-2 rb:py-1 rb:text-left">{children}</td>,
input: ({ children, ...props }: { children: string }) => {
code: ({ children, className, ...props }: any) => <Code children={String(children)} className={className || ''} {...props} />,
img: ({ src, alt, ...props }: any) => <Image src={src} alt={alt} {...props} />,
video: ({ src, ...props }: any) => <VideoBlock node={{ children: [{ properties: { src: src || '' } }] }} {...props} />,
audio: ({ src, ...props }: any) => <AudioBlock node={{ children: [{ properties: { src: src || '' } }] }} {...props} />,
a: ({ href, children, ...props }: any) => <Link href={href || '#'} {...props}>{children}</Link>,
button: ({ children, ...props }: any) => <RbButton node={{ children }}>{[children]}</RbButton>,
table: ({ children, ...props }: any) => <table className="rb:border rb:border-[#D9D9D9] rb:mb-2" {...props}>{children}</table>,
tr: ({ children, ...props }: any) => <tr className="rb:border rb:border-[#D9D9D9]" {...props}>{children}</tr>,
th: ({ children, ...props }: any) => <th className="rb:border rb:border-[#D9D9D9] rb:px-2 rb:py-1 rb:text-left rb:font-bold" {...props}>{children}</th>,
td: ({ children, ...props }: any) => <td className="rb:border rb:border-[#D9D9D9] rb:px-2 rb:py-1 rb:text-left" {...props}>{children}</td>,
input: ({ children, ...props }: any) => {
switch (props.type) {
case 'color':
return <ColorPicker {...props} />
@@ -74,7 +79,7 @@ const components = {
return <Slider {...props} />
case 'submit':
case 'button':
return <RbButton {...props}>{props.value}</RbButton>
return <RbButton node={{ children: props.value || children }}>{[props.value || children]}</RbButton>
case 'checkbox':
return <Checkbox {...props}>{children}</Checkbox>
case 'password':
@@ -85,37 +90,158 @@ const components = {
return <Input value={children} {...props} />
}
},
select: ({ children, ...props }: { children: string }) => <Select style={{width: '100%'}} {...props}>{children}</Select>,
textarea: ({ children, ...props }: { children: string }) => <Input.TextArea {...props}>{children}</Input.TextArea>,
form: ({ children }: { children: string }) => <Form>{children}</Form>,
select: ({ children, ...props }: any) => <Select style={{width: '100%'}} {...props}>{children}</Select>,
textarea: ({ children, ...props }: any) => <Input.TextArea {...props}>{children}</Input.TextArea>,
form: ({ children, ...props }: any) => <Form {...props}>{children}</Form>,
}
const RbMarkdown: FC<RbMarkdownProps> = ({
content,
showHtmlComments = false,
editable = false,
onContentChange,
onSave,
}) => {
const [isEditing, setIsEditing] = useState(editable) // 如果可编辑,默认进入编辑模式
const [editContent, setEditContent] = useState(content)
const textareaRef = useRef<any>(null)
// 当外部 content 变化时,同步更新编辑内容
useEffect(() => {
setEditContent(content)
}, [content])
// 当editable变化时自动切换编辑状态
useEffect(() => {
if (editable) {
setIsEditing(true)
// 延迟聚焦,确保 textarea 已渲染
setTimeout(() => {
textareaRef.current?.focus()
}, 100)
}
}, [editable])
// 进入编辑模式
const handleEdit = () => {
setIsEditing(true)
setEditContent(content)
// 延迟聚焦,确保 textarea 已渲染
setTimeout(() => {
textareaRef.current?.focus()
}, 100)
}
// 保存编辑
const handleSave = () => {
onContentChange?.(editContent)
onSave?.(editContent)
if (!editable) {
setIsEditing(false) // 只有在非强制编辑模式下才退出编辑
}
}
// 取消编辑
const handleCancel = () => {
setEditContent(content) // 恢复原内容
if (!editable) {
setIsEditing(false) // 只有在非强制编辑模式下才退出编辑
}
}
// 处理 textarea 内容变化
const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newContent = e.target.value
setEditContent(newContent)
// 实时回调内容变化
onContentChange?.(newContent)
}
// 根据参数决定是否将 HTML 注释转换为可见文本
// 使用特殊的 markdown 语法来显示注释,避免被 rehype-raw 过滤
const processedContent = showHtmlComments
? content.replace(/<!--([\s\S]*?)-->/g, (_match, commentContent) => {
? (isEditing ? editContent : content).replace(/<!--([\s\S]*?)-->/g, (_match, commentContent) => {
// 转换为带样式的文本,使用 <span class="html-comment"> 标记
const escaped = commentContent.trim().replace(/</g, '&lt;').replace(/>/g, '&gt;')
return `<span class="html-comment">&lt;!-- ${escaped} --&gt;</span>`
})
: content
: (isEditing ? editContent : content)
// 如果是编辑模式,显示 textarea
if (isEditing) {
return (
<div className="rb:relative">
<style>{`
.html-comment {
color: #999;
font-size: 0.9em;
}
`}</style>
{/* 编辑工具栏 - 只在非强制编辑模式下显示 */}
{!editable && (
<div className="rb:flex rb:justify-end rb:gap-2 rb:mb-2">
<Button
type="primary"
size="small"
icon={<SaveOutlined />}
onClick={handleSave}
>
</Button>
<Button
size="small"
icon={<CloseOutlined />}
onClick={handleCancel}
>
</Button>
</div>
)}
{/* 编辑区域 */}
<Input.TextArea
ref={textareaRef}
value={editContent}
onChange={handleTextareaChange}
rows={editable ? 5 : 10}
className="rb:font-mono rb:text-sm"
placeholder="请输入 Markdown 内容..."
style={{ resize: 'vertical' }}
/>
</div>
)
}
// 预览模式
return (
<div>
<div className="rb:relative rb:group">
<style>{`
.html-comment {
color: #999;
font-size: 0.9em;
}
`}</style>
{/* 编辑按钮 - 只在非强制编辑模式且鼠标悬停时显示 */}
{!editable && (
<div className="rb:absolute rb:top-0 rb:right-0 rb:opacity-0 group-hover:rb:opacity-100 rb:transition-opacity rb:z-10">
<Button
type="text"
size="small"
icon={<EditOutlined />}
onClick={handleEdit}
className="rb:bg-white rb:shadow-sm"
>
</Button>
</div>
)}
<ReactMarkdown
// allowElement={[]}
// allowedElements={[]}
components={components}
components={components as any}
disallowedElements={['script', 'iframe', 'head', 'html', 'meta', 'link', 'style', 'body']}
rehypePlugins={[
RehypeKatex,

View File

@@ -29,7 +29,7 @@ interface PageScrollListProps {
const PageScrollList = forwardRef<PageScrollListRef, PageScrollListProps>(({
renderItem,
query = {},
query,
url,
column = 4,
className = '',
@@ -51,11 +51,11 @@ const PageScrollList = forwardRef<PageScrollListRef, PageScrollListProps>(({
request.get(url, {
page: page,
pagesize: PAGE_SIZE,
...query,
...(query||{}),
})
.then((res) => {
const response = res as ApiResponse;
const results = Array.isArray(response.items) ? response.items : Array.isArray(response.hosts) ? response.hosts : Array.isArray(response) ? response : [];
const results = Array.isArray(response.items) ? response.items : Array.isArray(response) ? response : [];
if (flag) {
setData(results);
} else {

View File

@@ -15,6 +15,7 @@ interface RadioCardProps extends Omit<RadioGroupProps, 'onChange'> {
onValueChange?: (value: string | null | undefined, option?: RadioCardOption) => void;
onChange?: (value: string | null | undefined, option?: RadioCardOption) => void;
itemRender?: (option: RadioCardOption) => ReactNode;
allowClear?: boolean;
}
const RadioGroupCard: FC<RadioCardProps> = ({
@@ -22,7 +23,8 @@ const RadioGroupCard: FC<RadioCardProps> = ({
value,
onValueChange,
onChange,
itemRender
itemRender,
allowClear = true
}) => {
// 监听value变化
useEffect(() => {
@@ -34,23 +36,27 @@ const RadioGroupCard: FC<RadioCardProps> = ({
const handleChange = (option: RadioCardOption) => {
if (option.disabled) return
if (onChange) {
onChange(value === option.value ? null : String(option.value), value === option.value ? undefined : option);
if (allowClear && value === option.value) {
onChange(null, undefined);
} else {
onChange(String(option.value), option);
}
}
}
return (
<div className={`rb:grid rb:grid-cols-${options.length} rb:gap-[12px]`}>
<div className={`rb:grid rb:grid-cols-${options.length} rb:gap-3`}>
{options.map(option => (
<div key={String(option.value)} className={clsx("rb:border rb:rounded-[8px] rb:w-full rb:p-[20px_12px] rb:text-center rb:cursor-pointer", {
<div key={String(option.value)} className={clsx("rb:border rb:rounded-lg rb:w-full rb:p-[20px_12px] rb:text-center rb:cursor-pointer", {
'rb:bg-[rgba(21,94,239,0.06)] rb:border-[#155EEF]': option.value === value,
'rb:border-[#EBEBEB] rb:bg-[#ffffff]': option.value !== value,
'rb:opacity-[0.75]': option.disabled
})} onClick={() => handleChange(option)}>
{itemRender ? itemRender(option) : (
<>
{option.icon && <img src={option.icon} className="rb:w-[40px] rb:h-[40px] rb:mb-[12px] rb:m-[0_auto]" />}
{option.icon && <img src={option.icon} className="rb:w-10 rb:h-10 rb:mb-3 rb:m-[0_auto]" />}
<div className="rb:text-[14px] rb:font-medium">{option.label}</div>
<div className="rb:mt-[6px] rb:text-[#5B6167] rb:text-[12px] rb:font-regular">{option.labelDesc}</div>
<div className="rb:mt-1.5 rb:text-[#5B6167] rb:text-[12px] rb:font-regular">{option.labelDesc}</div>
</>
)}
</div>

View File

@@ -3,7 +3,7 @@ import { type FC, type ReactNode } from 'react'
interface RbAlertProps {
color?: 'blue' | 'green' | 'orange' | 'purple',
children: ReactNode | string;
icon: ReactNode;
icon?: ReactNode;
className?: string;
}
@@ -16,8 +16,8 @@ const colors = {
const RbAlert: FC<RbAlertProps> = ({ color = 'blue', icon, className, children }) => {
return (
<div className={`${colors[color]} ${className} rb:p-[6px_9px] rb:flex rb:items-center rb:text-[12px] rb:font-regular rb:leading-[16px] rb:border-[1px] rb:rounded-[6px]`}>
{icon && <span className="rb:text-[16px] rb:mr-[9px]">{icon}</span>}
<div className={`${colors[color]} ${className} rb:p-[6px_9px] rb:flex rb:items-center rb:text-[12px] rb:font-regular rb:leading-4 rb:border rb:rounded-md`}>
{icon && <span className="rb:text-[16px] rb:mr-2.25">{icon}</span>}
{children}
</div>
)

View File

@@ -52,7 +52,7 @@ const RbCard: FC<RbCardProps> = ({
title={typeof title === 'function' ? title() : title ?
<div className="rb:flex rb:items-center">
{avatarUrl
? <img src={avatarUrl} className="rb:mr-[13px] rb:w-[48px] rb:h-[48px] rb:rounded-[8px]" />
? <img src={avatarUrl} className="rb:mr-3.25 rb:w-12 rb:h-12 rb:rounded-lg" />
: avatar ? avatar : null
}
<div className={

View File

@@ -0,0 +1,3 @@
.rb-modal .ant-modal-header {
margin-bottom: 24px;
}

View File

@@ -1,11 +1,19 @@
/*
* @Description:
* @Version: 0.0.1
* @Author: yujiangping
* @Date: 2025-12-16 10:19:18
* @LastEditors: yujiangping
* @LastEditTime: 2025-12-22 12:31:31
*/
import { type FC } from 'react'
import { Modal, type ModalProps } from 'antd'
import { useTranslation } from 'react-i18next'
const RbModal: FC<ModalProps> = ({
onOk,
onCancel,
children,
className,
...props
}) => {
const { t } = useTranslation()
@@ -16,9 +24,11 @@ const RbModal: FC<ModalProps> = ({
cancelText={t('common.cancel')}
onOk={onOk}
destroyOnHidden={true}
className={`rb-modal ${className || ''}`}
maskClosable={false}
{...props}
>
<div className='rb:max-h-[550px] rb:overflow-y-auto rb:overflow-x-hidden'>
<div className='rb:max-h-137.5 rb:overflow-y-auto rb:overflow-x-hidden'>
{children}
</div>
</Modal>

View File

@@ -21,11 +21,11 @@ import modelActiveIcon from '@/assets/images/menu/model_active.svg';
import memoryIcon from '@/assets/images/menu/memory.svg';
import memoryActiveIcon from '@/assets/images/menu/memory_active.svg';
import spaceIcon from '@/assets/images/menu/space.svg';
import spaceActiveIcon from '@/assets/images/menu/space_acitve.svg';
import spaceActiveIcon from '@/assets/images/menu/space_active.svg';
import userIcon from '@/assets/images/menu/user.svg';
import userActiveIcon from '@/assets/images/menu/user_active.svg';
import userMemoryIcon from '@/assets/images/menu/userMemory.svg';
import userMemoryActiveIcon from '@/assets/images/menu/userMemory_acitve.svg';
import userMemoryActiveIcon from '@/assets/images/menu/userMemory_active.svg';
import applicationIcon from '@/assets/images/menu/application.svg';
import applicationActiveIcon from '@/assets/images/menu/application_active.svg';
import knowledgeIcon from '@/assets/images/menu/knowledge.svg';
@@ -34,6 +34,12 @@ import memoryConversationIcon from '@/assets/images/menu/memoryConversation.svg'
import memoryConversationActiveIcon from '@/assets/images/menu/memoryConversation_active.svg';
import memberIcon from '@/assets/images/menu/member.svg';
import memberActiveIcon from '@/assets/images/menu/member_active.svg';
import toolIcon from '@/assets/images/menu/tool.png';
import toolActiveIcon from '@/assets/images/menu/tool_active.png';
import apiKeyIcon from '@/assets/images/menu/apiKey.png';
import apiKeyActiveIcon from '@/assets/images/menu/apiKey_active.png';
import pricingIcon from '@/assets/images/menu/pricing.svg'
import pricingActiveIcon from '@/assets/images/menu/pricing_active.svg'
// 图标路径映射表
const iconPathMap: Record<string, string> = {
@@ -57,6 +63,12 @@ const iconPathMap: Record<string, string> = {
'memoryConversationActive': memoryConversationActiveIcon,
'member': memberIcon,
'memberActive': memberActiveIcon,
'tool': toolIcon,
'toolActive': toolActiveIcon,
'apiKey': apiKeyIcon,
'apiKeyActive': apiKeyActiveIcon,
'pricing': pricingIcon,
'pricingActive': pricingActiveIcon
};
const { Sider } = Layout;

View File

@@ -19,6 +19,7 @@ interface TableComponentProps extends Omit<TableProps, 'pagination'> {
isScroll?: boolean;
scrollX?: number | string | true; // 支持自定义横向滚动宽度
scrollY?: number | string; // 支持自定义纵向滚动高度
currentPageKey?: string;
}
export interface TableRef {
loadData: () => void;
@@ -48,6 +49,7 @@ const TableComponent = forwardRef<TableRef, TableComponentProps>(({
isScroll = false,
scrollX,
scrollY,
currentPageKey = 'page',
...props
}, ref) => {
const { t } = useTranslation();
@@ -86,7 +88,7 @@ const TableComponent = forwardRef<TableRef, TableComponentProps>(({
...currentPagination,
...pageData,
})
params = {...params, ...pageData}
params = { ...params, ...pageData, [currentPageKey]: pageData.page}
}
setLoading(true)
// 构建查询参数并调用API
@@ -95,7 +97,7 @@ const TableComponent = forwardRef<TableRef, TableComponentProps>(({
// 支持两种响应格式:直接返回 total 或在 page 对象中返回
const totalCount = res.page?.total ?? res.total ?? 0;
setTotal(totalCount)
setData(Array.isArray(res.items) ? res.items : Array.isArray(res.hosts) ? res.hosts : res || [])
setData(Array.isArray(res.items) ? res.items : Array.isArray(res.hosts) ? res.hosts : Array.isArray(res.list) ? res.list : res || [])
setLoading(false)
})
.catch(err => {

View File

@@ -1,6 +1,6 @@
import { type FC, type ReactNode } from 'react'
interface TagProps {
export interface TagProps {
color?: 'processing' | 'error' | 'success' | 'warning' | 'default',
children: ReactNode;
className?: string;
@@ -16,7 +16,7 @@ const colors = {
const Tag: FC<TagProps> = ({ color = 'processing', children, className }) => {
return (
<span className={`rb:inline-block rb:px-[4px] rb:py-[2px] rb:rounded-[4px] rb:text-[12px] rb:font-regular! rb:leading-[16px] rb:border-[1px] ${colors[color]} ${className || ''}`}>
<span className={`rb:inline-block rb:px-1 rb:py-0.5 rb:rounded-sm rb:text-[12px] rb:font-regular! rb:leading-4 rb:border ${colors[color]} ${className || ''}`}>
{children}
</span>
)

View File

@@ -38,6 +38,8 @@ interface UploadFilesProps extends Omit<UploadProps, 'onChange'> {
maxCount?: number;
/** 是否支持拖拽上传默认为false */
isCanDrag?: boolean;
/** 自定义移除文件回调 */
onRemove?: (file: UploadFile) => boolean | void | Promise<boolean | void>;
}
const ALL_FILE_TYPE: {
[key: string]: string;
@@ -77,6 +79,7 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
isAutoUpload = true,
maxCount = 1,
isCanDrag = false,
onRemove: customOnRemove,
...props
}, ref) => {
const { t } = useTranslation();
@@ -86,11 +89,20 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
// 处理文件移除
const handleRemove = (file: UploadFile) => {
// 如果有自定义的 onRemove 回调,先执行它
if (customOnRemove) {
const result = customOnRemove(file);
// 如果返回 false阻止移除
if (result === false) {
return false;
}
}
confirm({
title: '确定要删除此文件吗?',
okText: '确定',
title: `${t('common.confirmRemoveFile')}`,
okText: `${t('common.confirm')}`,
okType: 'danger',
cancelText: '取消',
cancelText: `${t('common.cancel')}`,
onOk: () => {
const newFileList = fileList.filter((item) => item.uid !== file.uid);
setFileList(newFileList);
@@ -236,7 +248,7 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
<div key={file.uid} className="rb:relative rb:w-full rb:pt-[8px] rb:pl-[10px] rb:pr-[10px] rb-pb-[10px] rb:border-1 rb:border-[#EBEBEB] rb:rounded rb:p-2 rb:mt-2 rb:bg-white">
<div className="rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:mb-[2px]">
{file.name}
<span className="rb:text-[#5B6167]" onClick={() => actions?.remove()}>Cancel</span>
<span className="rb:text-[#5B6167] rb:cursor-pointer" onClick={() => actions?.remove()}>Cancel</span>
</div>
<Progress percent={file.percent || 0} strokeColor={file.status === 'error' ? '#FF5D34' : '#155EEF'} size="small" showInfo={false} />
</div>

View File

@@ -0,0 +1,319 @@
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useMenu } from '@/store/menu';
import type { MenuItem } from '@/store/menu';
export interface BreadcrumbItem {
id: string;
name: string;
type?: 'knowledgeBase' | 'folder' | 'document';
}
export interface BreadcrumbPath {
knowledgeBaseFolderPath: BreadcrumbItem[]; // 知识库文件夹路径
knowledgeBase?: BreadcrumbItem; // 知识库信息
documentFolderPath: BreadcrumbItem[]; // 文档文件夹路径
document?: BreadcrumbItem; // 文档信息
}
export interface BreadcrumbOptions {
onKnowledgeBaseMenuClick?: () => void;
onKnowledgeBaseFolderClick?: (folderId: string, folderPath: BreadcrumbItem[]) => void;
// 新增:区分面包屑类型
breadcrumbType?: 'list' | 'detail';
}
export const useBreadcrumbManager = (options?: BreadcrumbOptions) => {
const { allBreadcrumbs, setCustomBreadcrumbs } = useMenu();
const navigate = useNavigate();
const updateBreadcrumbs = useCallback((breadcrumbPath: BreadcrumbPath) => {
const breadcrumbType = options?.breadcrumbType || 'list';
// 对于详情页面,直接使用固定的知识库管理面包屑,不依赖可能被污染的 allBreadcrumbs
let baseBreadcrumbs: MenuItem[] = [];
if (breadcrumbType === 'detail') {
// 详情页面:始终使用固定的知识库管理面包屑
baseBreadcrumbs = [
{
id: 6,
parent: 0,
code: 'knowledge',
label: '知识库',
i18nKey: 'menu.knowledgeManagement',
path: '/knowledge-base',
enable: true,
display: true,
level: 1,
sort: 0,
icon: null,
iconActive: null,
menuDesc: null,
deleted: null,
updateTime: 0,
new_: null,
keepAlive: false,
master: null,
disposable: false,
appSystem: null,
subs: [],
}
];
} else {
// 列表页面:从 space 获取基础面包屑,但确保包含知识库管理
const spaceBreadcrumbs = allBreadcrumbs['space'] || [];
const knowledgeBaseMenuIndex = spaceBreadcrumbs.findIndex(item => item.path === '/knowledge-base');
if (knowledgeBaseMenuIndex >= 0) {
baseBreadcrumbs = spaceBreadcrumbs.slice(0, knowledgeBaseMenuIndex + 1);
} else {
// 如果没有找到知识库菜单,使用默认的知识库管理面包屑
baseBreadcrumbs = [
{
id: 6,
parent: 0,
code: 'knowledge',
label: '知识库',
i18nKey: 'menu.knowledgeManagement',
path: '/knowledge-base',
enable: true,
display: true,
level: 1,
sort: 0,
icon: null,
iconActive: null,
menuDesc: null,
deleted: null,
updateTime: 0,
new_: null,
keepAlive: false,
master: null,
disposable: false,
appSystem: null,
subs: [],
}
];
}
}
const filteredBaseBreadcrumbs = baseBreadcrumbs;
// 给"知识库管理"添加点击事件
const breadcrumbsWithClick = filteredBaseBreadcrumbs.map((item) => {
if (item.path === '/knowledge-base') {
return {
...item,
onClick: (e?: React.MouseEvent) => {
e?.preventDefault();
e?.stopPropagation();
if (options?.onKnowledgeBaseMenuClick) {
// 如果提供了回调函数,执行回调
options.onKnowledgeBaseMenuClick();
} else if (breadcrumbType === 'detail') {
// 知识库详情页面:没有回调函数时,返回到知识库列表页面
navigate('/knowledge-base', {
state: {
resetToRoot: true,
}
});
}
return false;
},
};
}
return item;
});
let customBreadcrumbs: MenuItem[] = [...breadcrumbsWithClick];
if (breadcrumbType === 'list') {
// 知识库列表页面:只显示知识库文件夹路径
customBreadcrumbs = [
...breadcrumbsWithClick,
...breadcrumbPath.knowledgeBaseFolderPath.map((folder, index) => ({
id: 0,
parent: 0,
code: null,
label: folder.name,
i18nKey: null,
path: null,
enable: true,
display: true,
level: 0,
sort: 0,
icon: null,
iconActive: null,
menuDesc: null,
deleted: null,
updateTime: 0,
new_: null,
keepAlive: false,
master: null,
disposable: false,
appSystem: null,
subs: [],
onClick: (e?: React.MouseEvent) => {
e?.preventDefault();
e?.stopPropagation();
// 如果有回调函数,直接调用回调函数来更新状态
if (options?.onKnowledgeBaseFolderClick) {
options.onKnowledgeBaseFolderClick(folder.id, breadcrumbPath.knowledgeBaseFolderPath.slice(0, index + 1));
} else {
// 否则使用导航(兜底逻辑)
navigate('/knowledge-base', {
state: {
navigateToFolder: folder.id,
folderPath: breadcrumbPath.knowledgeBaseFolderPath.slice(0, index + 1)
}
});
}
return false;
},
})),
];
} else {
// 知识库详情页面:显示知识库名称 + 文档文件夹路径 + 文档名称
customBreadcrumbs = [
...breadcrumbsWithClick,
// 添加知识库名称
...(breadcrumbPath.knowledgeBase ? [{
id: 0,
parent: 0,
code: null,
label: breadcrumbPath.knowledgeBase.name,
i18nKey: null,
path: null,
enable: true,
display: true,
level: 0,
sort: 0,
icon: null,
iconActive: null,
menuDesc: null,
deleted: null,
updateTime: 0,
new_: null,
keepAlive: false,
master: null,
disposable: false,
appSystem: null,
subs: [],
onClick: (e?: React.MouseEvent) => {
e?.preventDefault();
e?.stopPropagation();
// 返回到知识库详情页的根目录
const navigationState = {
fromKnowledgeBaseList: true,
knowledgeBaseFolderPath: breadcrumbPath.knowledgeBaseFolderPath,
resetToRoot: true, // 添加重置到根目录的标志
refresh: true, // 添加刷新标志
timestamp: Date.now(), // 添加时间戳确保状态变化
};
// 使用当前页面路径进行导航,避免不必要的路由变化
const currentPath = window.location.pathname;
const targetPath = `/knowledge-base/${breadcrumbPath.knowledgeBase!.id}/private`;
if (currentPath === targetPath) {
// 如果已经在目标页面,直接更新状态而不导航
navigate(targetPath, {
state: navigationState,
replace: true // 使用 replace 避免历史记录堆积
});
} else {
// 如果不在目标页面,正常导航
navigate(targetPath, {
state: navigationState
});
}
return false;
},
}] : []),
// 添加文档文件夹路径
...breadcrumbPath.documentFolderPath.map((folder, index) => ({
id: 0,
parent: 0,
code: null,
label: folder.name,
i18nKey: null,
path: null,
enable: true,
display: true,
level: 0,
sort: 0,
icon: null,
iconActive: null,
menuDesc: null,
deleted: null,
updateTime: 0,
new_: null,
keepAlive: false,
master: null,
disposable: false,
appSystem: null,
subs: [],
onClick: (e?: React.MouseEvent) => {
e?.preventDefault();
e?.stopPropagation();
// 返回到知识库详情页的对应文件夹
const navigationState = {
fromKnowledgeBaseList: true,
knowledgeBaseFolderPath: breadcrumbPath.knowledgeBaseFolderPath,
navigateToDocumentFolder: folder.id,
documentFolderPath: breadcrumbPath.documentFolderPath.slice(0, index + 1),
refresh: true, // 添加刷新标志
timestamp: Date.now(), // 添加时间戳确保状态变化
};
navigate(`/knowledge-base/${breadcrumbPath.knowledgeBase!.id}/private`, {
state: navigationState,
replace: true // 使用 replace 避免历史记录堆积
});
return false;
},
})),
// 添加文档名称(如果存在)
...(breadcrumbPath.document ? [{
id: 0,
parent: 0,
code: null,
label: breadcrumbPath.document.name,
i18nKey: null,
path: null,
enable: true,
display: true,
level: 0,
sort: 0,
icon: null,
iconActive: null,
menuDesc: null,
deleted: null,
updateTime: 0,
new_: null,
keepAlive: false,
master: null,
disposable: false,
appSystem: null,
subs: [],
// 文档名称不可点击
}] : []),
];
}
// 根据面包屑类型使用不同的键,实现独立的面包屑路径
const breadcrumbKey = breadcrumbType === 'list' ? 'space' : 'space-detail';
setCustomBreadcrumbs(customBreadcrumbs, breadcrumbKey);
}, [setCustomBreadcrumbs, navigate, options?.breadcrumbType, options?.onKnowledgeBaseMenuClick, options?.onKnowledgeBaseFolderClick]);
return {
updateBreadcrumbs,
};
};

View File

@@ -11,8 +11,10 @@ export const checkAuthStatus = (): boolean => {
// 递归检查路由是否存在于菜单数据中
export const checkRoutePermission = (menus: MenuItem[], currentPath: string): boolean => {
// 首页默认有权限
if (currentPath === '/' || currentPath.includes('knowledge-detail')) return true;
// 首页和知识库相关页面默认有权限
if (currentPath === '/' || currentPath.includes('knowledge-detail') || currentPath.includes('knowledge-base')) {
return true;
}
for (const menu of menus) {
// 检查当前菜单的path是否匹配
@@ -26,6 +28,7 @@ export const checkRoutePermission = (menus: MenuItem[], currentPath: string): bo
}
}
}
return false;
};
@@ -52,7 +55,7 @@ export const useRouteGuard = (source: 'space' | 'manage') => {
const hasPermission = checkRoutePermission(menus, location.pathname);
if (!hasPermission) {
// 无权限访问该路由,重定向到无权限页面
// navigate('/not-found', { replace: true });
// navigate('/no-permission', { replace: true });
}
}
}, [navigate, location.pathname, location.search, location.hash, menus]);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -10,19 +10,19 @@ import routesConfig from './routes.json';
// 递归收集所有路由中的element
function collectElements(routes: RouteConfig[]): Set<string> {
const elements = new Set<string>();
function traverse(routeList: RouteConfig[]) {
routeList.forEach(route => {
// 添加当前路由的element
elements.add(route.element);
// 递归处理子路由
if (route.children && route.children.length > 0) {
traverse(route.children);
}
});
}
traverse(routes);
return elements;
}
@@ -38,6 +38,7 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
Home: lazy(() => import('@/views/Home')),
UserMemory: lazy(() => import('@/views/UserMemory')),
UserMemoryDetail: lazy(() => import('@/views/UserMemoryDetail')),
Neo4jUserMemoryDetail: lazy(() => import('@/views/UserMemoryDetail/Neo4j')),
MemberManagement: lazy(() => import('@/views/MemberManagement')),
MemoryManagement: lazy(() => import('@/views/MemoryManagement')),
ForgettingEngine: lazy(() => import('@/views/ForgettingEngine')),
@@ -54,10 +55,18 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
UserManagement: lazy(() => import('@/views/UserManagement')),
ModelManagement: lazy(() => import('@/views/ModelManagement')),
SpaceManagement: lazy(() => import('@/views/SpaceManagement')),
ApiKeyManagement: lazy(() => import('@/views/ApiKeyManagement')),
EmotionEngine: lazy(() => import('@/views/EmotionEngine')),
StatementDetail: lazy(() => import('@/views/UserMemoryDetail/pages/StatementDetail')),
SelfReflectionEngine: lazy(() => import('@/views/SelfReflectionEngine')),
OrderPayment: lazy(() => import('@/views/OrderPayment')),
OrderHistory: lazy(() => import('@/views/OrderHistory')),
Pricing: lazy(() => import('@/views/Pricing')),
ToolManagement: lazy(() => import('@/views/ToolManagement')),
Login: lazy(() => import('@/views/Login')),
InviteRegister: lazy(() => import('@/views/InviteRegister')),
NoPermission: lazy(() => import('@/views/NoPermission')),
NotFound: lazy(() => import('@/views/NotFound')),
NotFound: lazy(() => import('@/views/NotFound'))
};
// 检查并报告缺失的组件
@@ -87,12 +96,12 @@ const generateRoutes = (routes: RouteConfig[]): ReactNode => {
// 获取组件
const componentKey = route.element as keyof typeof componentMap;
const Component = componentMap[componentKey];
if (!Component) {
console.error(`Component ${route.element} not found in componentMap`);
return null;
}
// 如果有子路由
if (route.children) {
return (
@@ -101,12 +110,12 @@ const generateRoutes = (routes: RouteConfig[]): ReactNode => {
</Route>
);
}
// 如果有path属性则为普通路由
if (route.path) {
return <Route key={index} path={route.path} element={<Component />} />;
}
return null;
});
};

View File

@@ -5,6 +5,10 @@
{ "path": "/user-management", "element": "UserManagement" },
{ "path": "/model", "element": "ModelManagement" },
{ "path": "/space", "element": "SpaceManagement" },
{ "path": "/tool", "element": "ToolManagement" },
{ "path": "/pricing", "element": "Pricing" },
{ "path": "/order-pay", "element": "OrderPayment" },
{ "path": "/orders", "element": "OrderHistory" },
{ "path": "/no-permission", "element": "NoPermission" }
]
},
@@ -25,6 +29,10 @@
{ "path": "/knowledge-base/:knowledgeBaseId/share", "element": "Share" },
{ "path": "/knowledge-base/:knowledgeBaseId/create-dataset", "element": "CreateDataset" },
{ "path": "/knowledge-base/:knowledgeBaseId/DocumentDetails", "element": "DocumentDetails" },
{ "path": "/api-key", "element": "ApiKeyManagement" },
{ "path": "/emotion-engine/:id", "element": "EmotionEngine" },
{ "path": "/reflection-engine/:id", "element": "SelfReflectionEngine" },
{ "path": "/statement/:id", "element": "StatementDetail" },
{ "path": "/no-permission", "element": "NoPermission" },
{ "path": "/*", "element": "NotFound" }
]
@@ -33,7 +41,8 @@
"element": "BasicLayout",
"children": [
{ "path": "/application/config/:id", "element": "ApplicationConfig" },
{ "path": "/conversation/:token", "element": "Conversation" }
{ "path": "/conversation/:token", "element": "Conversation" },
{ "path": "/user-memory/neo4j/:id", "element": "Neo4jUserMemoryDetail" }
]
},
{

View File

@@ -26,6 +26,59 @@
"sort": 0,
"subs": []
},
{
"id": 7,
"parent": 0,
"code": "tool",
"label": "工具管理",
"i18nKey": "menu.toolManagement",
"path": "/tool",
"enable": true,
"display": true,
"level": 1,
"sort": 0,
"subs": []
},
{
"id": 6,
"parent": 0,
"code": "pricing",
"label": "收费管理",
"i18nKey": "menu.pricing",
"path": "/pricing",
"enable": true,
"display": true,
"level": 1,
"sort": 0,
"subs": [
{
"id": 61,
"parent": 6,
"code": "order",
"label": "订单支付",
"i18nKey": "menu.orderPayment",
"path": "/order-pay",
"enable": true,
"display": false,
"level": 1,
"sort": 0,
"subs": []
},
{
"id": 62,
"parent": 6,
"code": "orderHistory",
"label": "订单记录",
"i18nKey": "menu.orderHistory",
"path": "/orders",
"enable": true,
"display": false,
"level": 1,
"sort": 0,
"subs": []
}
]
},
{
"id": 3,
"parent": 0,
@@ -183,6 +236,32 @@
"level": 1,
"sort": 0,
"subs": null
},
{
"id": 72,
"parent": 7,
"code": "emotionEngine",
"label": "情感引擎",
"i18nKey": "menu.emotionEngine",
"path": "/emotion-engine/:id",
"enable": true,
"display": false,
"level": 1,
"sort": 0,
"subs": null
},
{
"id": 72,
"parent": 7,
"code": "selfReflectionEngine",
"label": "反思引擎",
"i18nKey": "menu.selfReflectionEngine",
"path": "/reflection-engine/:id",
"enable": true,
"display": false,
"level": 1,
"sort": 0,
"subs": null
}
]
},
@@ -205,12 +284,38 @@
"code": "userMemoryDetail",
"label": "记忆详情",
"i18nKey": "menu.userMemoryDetail",
"path": "/user-memory/:id",
"path": "/user-memory/neo4j/:id",
"enable": true,
"display": false,
"level": 2,
"sort": 0,
"subs": null
"subs": [
{
"id": 811,
"parent": 81,
"code": "statementDetail",
"label": "记忆详情",
"i18nKey": "menu.statementDetail",
"path": "/statement/:id",
"enable": true,
"display": false,
"level": 3,
"sort": 0,
"subs": null
}
]
},
{
"id": 81,
"parent": 8,
"code": "userMemoryDetail",
"label": "记忆详情",
"i18nKey": "menu.userMemoryDetail",
"path": "/user-memory/:id",
"enable": true,
"display": false,
"level": 2,
"sort": 0
}
]
},
@@ -243,6 +348,21 @@
"icon": null,
"iconActive": null,
"subs": null
},
{
"id": 11,
"parent": 0,
"code": "apiKey",
"label": "API KEY管理",
"i18nKey": "menu.apiKeyManagement",
"path": "/api-key",
"enable": true,
"display": true,
"level": 1,
"sort": 0,
"icon": null,
"iconActive": null,
"subs": null
}
]
}

View File

@@ -32,7 +32,7 @@ interface MenuState {
allBreadcrumbs: Record<'space' | 'manage' | string, MenuItem[]>;
loadMenus: (source: 'space' | 'manage') => void;
updateBreadcrumbs: (keyPath: string[], source: 'space' | 'manage') => void;
setCustomBreadcrumbs: (breadcrumbs: MenuItem[], source: 'space' | 'manage') => void;
setCustomBreadcrumbs: (breadcrumbs: MenuItem[], source: string) => void;
}
const initBreadcrumbs = localStorage.getItem('breadcrumbs') || '[]'
@@ -57,20 +57,47 @@ export const useMenu = create<MenuState>((set, get) => ({
const { allMenus } = get()
const menus = allMenus[source] || []
let result: MenuItem[] = []
const matchedMenu: MenuItem | undefined = menus.find(menu => menu.path === paths[paths.length - 1] || `${menu.id}` === paths[1]);
if (matchedMenu) {
let matchedSubMenu: MenuItem | undefined = undefined;
if (paths.length > 1 && matchedMenu?.subs?.length) {
matchedSubMenu = matchedMenu.subs.find(menu => menu.path === paths[0]);
console.log('updateBreadcrumbs paths:', paths);
if (paths.length === 3) {
// 三级菜单:[subSubPath, subId, menuId]
const menuId = paths[2];
const subId = paths[1];
const subSubPath = paths[0];
const matchedMenu = menus.find(menu => `${menu.id}` === menuId);
if (matchedMenu && matchedMenu.subs) {
const matchedSub = matchedMenu.subs.find(sub => `${sub.id}` === subId);
if (matchedSub && matchedSub.subs) {
const matchedSubSub = matchedSub.subs.find(subSub => subSub.path === subSubPath);
if (matchedSubSub) {
result = [
{ ...matchedMenu, subs: null },
{ ...matchedSub, subs: null },
{ ...matchedSubSub, subs: null }
];
}
}
}
result = [
{ ...matchedMenu, subs: null },
matchedSubMenu
].filter(item => item !== undefined) as MenuItem[]
} else {
result = [] as MenuItem[]
// 原有逻辑处理一级和二级菜单
const matchedMenu: MenuItem | undefined = menus.find(menu => menu.path === paths[paths.length - 1] || `${menu.id}` === paths[1]);
if (matchedMenu) {
let matchedSubMenu: MenuItem | undefined = undefined;
if (paths.length > 1 && matchedMenu?.subs?.length) {
matchedSubMenu = matchedMenu.subs.find(menu => menu.path === paths[0]);
}
result = [
{ ...matchedMenu, subs: null },
matchedSubMenu
].filter(item => item !== undefined) as MenuItem[]
} else {
result = [] as MenuItem[]
}
}
const allBreadcrumbs = { ...get().allBreadcrumbs, [source]: result }
set({ allBreadcrumbs })
localStorage.setItem('breadcrumbs', JSON.stringify(allBreadcrumbs))

View File

@@ -174,4 +174,10 @@ body {
}
.ant-breadcrumb a:hover {
background-color: transparent;
}
/* X6 节点样式 */
.x6-node foreignObject > body {
min-height: 100%;
max-height: 100%;
}

View File

@@ -0,0 +1,46 @@
/**
* API密钥替换工具
*/
const API_KEY_PATTERNS = {
service: /sk-service-[A-Za-z0-9_-]+/g,
agent: /sk-agent-[A-Za-z0-9_-]+/g,
multiAgent: /sk-multi_agent-[A-Za-z0-9_-]+/g,
workflow: /sk-workflow-[A-Za-z0-9_-]+/g
}
const API_KEY_PREFIX = {
service: 'sk-service-',
agent: 'sk-agent-',
multiAgent: 'sk-multi_agent-',
workflow: 'sk-workflow-'
}
/**
* 替换文本中的API密钥为*号
* @param text 原始文本
* @returns 替换后的文本
*/
export const maskApiKeys = (text: string): string => {
if (!text) return text
let result = text
Object.keys(API_KEY_PREFIX).map(type => {
const key = type as keyof typeof API_KEY_PREFIX
result = result.replace(API_KEY_PATTERNS[key as keyof typeof API_KEY_PREFIX], (match) => {
const prefixLength = API_KEY_PREFIX[key].length
const prefix = match.substring(0, prefixLength)
return prefix + '*'.repeat(match.length - prefixLength)
})
})
return result
}
/**
* 检测文本中是否包含API密钥
* @param text 待检测文本
* @returns 是否包含API密钥
*/
export const hasApiKeys = (text: string): boolean => {
return Object.values(API_KEY_PATTERNS).some(pattern => pattern.test(text))
}

View File

@@ -302,6 +302,10 @@ export const request = {
// 获取父级域名
const getParentDomain = () => {
const hostname = window.location.hostname
// 检查是否为IP地址
if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
return hostname
}
const parts = hostname.split('.')
return parts.length > 2 ? `.${parts.slice(-2).join('.')}` : hostname
}

Some files were not shown because too many files have changed in this diff Show More