Commit 3489b2a6 authored by David Yang's avatar David Yang

feat: init

parents
module.exports = {
extends: require.resolve('@umijs/max/eslint'),
};
/node_modules
/.env.local
/.umirc.local.ts
/config/config.local.ts
/src/.umi
/src/.umi-production
/src/.umi-test
/.umi
/.umi-production
/.umi-test
/dist
/.mfsu
.swc
.idea/*
.git/*
.DS_Store
npx --no-install max verify-commit $1
npx --no-install lint-staged --quiet
{
"*.{md,json}": [
"prettier --cache --write"
],
"*.{js,jsx}": [
"max lint --fix --eslint-only",
"prettier --cache --write"
],
"*.{css,less}": [
"max lint --fix --stylelint-only",
"prettier --cache --write"
],
"*.ts?(x)": [
"max lint --fix --eslint-only",
"prettier --cache --parser=typescript --write"
]
}
registry=https://registry.npmmirror.com/
node_modules
.umi
.umi-production
{
"printWidth": 80,
"singleQuote": true,
"trailingComma": "all",
"proseWrap": "never",
"overrides": [{ "files": ".prettierrc", "options": { "parser": "json" } }],
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-packagejson"]
}
module.exports = {
extends: require.resolve('@umijs/max/stylelint'),
};
import { defineConfig } from '@umijs/max';
export default defineConfig({
base: 'ai',
publicPath: '/ai/',
antd: {},
access: {},
model: {},
initialState: {},
request: {},
reactQuery: {
devtool: false,
},
layout: false,
routes: [
{
name: '首页',
path: '/',
// redirect: '/chat',
component: './Home',
},
// {
// name: '聊天',
// path: '/chat',
// component: './Home',
// },
// {
// name: '聊天',
// path: '/chat/:name',
// component: './Home',
// },
],
npmClient: 'pnpm',
proxy: {
'/api': {
target: 'https://awm.rsmchina.com.cn/api',
// target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
'/myapi/upload_invoices': {
target: 'http://1.94.111.115:5173',
changeOrigin: true,
secure: false,
pathRewrite: { '^/myapi': '' },
},
},
});
# README
`@umijs/max` 模板项目,更多功能参考 [Umi Max 简介](https://umijs.org/docs/max/introduce)
const users = [
{ id: 0, name: 'Umi', nickName: 'U', gender: 'MALE' },
{ id: 1, name: 'Fish', nickName: 'B', gender: 'FEMALE' },
];
export default {
'GET /api/v1/queryUserList': (req: any, res: any) => {
res.json({
success: true,
data: { list: users },
errorCode: 0,
});
},
'PUT /api/v1/user/': (req: any, res: any) => {
res.json({
success: true,
errorCode: 0,
});
},
};
{
"private": true,
"author": "Mars Wu <mars.wu@drigle.com>",
"scripts": {
"build": "max build",
"dev": "max dev",
"format": "prettier --cache --write .",
"postinstall": "max setup",
"prepare": "husky",
"setup": "max setup",
"start": "npm run dev"
},
"dependencies": {
"@ant-design/icons": "^5.0.1",
"@ant-design/pro-components": "^2.4.4",
"@ant-design/x": "^1.6.0",
"@ant-design/x-markdown": "2.0.0-alpha.5",
"@modelcontextprotocol/sdk": "^1.17.5",
"@umijs/max": "^4.4.12",
"antd": "^5.4.0",
"antd-style": "^3.7.1",
"dayjs": "^1.11.18",
"jszip": "^3.10.1",
"katex": "^0.16.22",
"lodash": "^4.17.21",
"marked": "^16.2.1",
"marked-katex-extension": "^5.1.5",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@types/react": "^18.0.33",
"@types/react-dom": "^18.0.11",
"husky": "^9",
"lint-staged": "^13.2.0",
"prettier": "^2.8.7",
"prettier-plugin-organize-imports": "^3.2.2",
"prettier-plugin-packagejson": "^2.4.3",
"typescript": "^5.0.3"
}
}
This diff is collapsed.
{
"name": "S-Agent",
"version": "1.0.26",
"build": "1"
}
export default (initialState) => {
// 在这里按照初始化数据定义项目中的权限,统一管理
// 参考文档 https://umijs.org/docs/max/access
const canSeeAdmin = !!(
initialState && initialState.name !== 'dontHaveAccess'
);
return {
canSeeAdmin,
};
};
import { defineApp, matchRoutes } from 'umi';
import { createTracking } from './services/DataService';
import {
collectAppInfo,
collectClientInfo,
collectUserInfo,
} from './utils/trackingHelper';
export const request = {
prefix: '/api',
timeout: 30000,
// requestInterceptors: [
// (config) => {
// const user = JSON.parse(localStorage.getItem('current_user') || 'null');
// const token = user?.token || localStorage.getItem('auth_token');
// const headers = {
// ContentType: 'application/json',
// ...config.headers,
// };
//
// if (token) {
// headers['Authorization'] = `Bearer ${token}`;
// }
//
// return {
// ...config,
// headers,
// };
// },
// ],
};
export const reactQuery = {
queryClient: {
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
},
};
/**
* 从路由配置中获取路由名称
*/
function getRouteName(clientRoutes, pathname) {
try {
const matches = matchRoutes(clientRoutes, pathname);
if (matches && matches.length > 0) {
const route = matches[matches.length - 1]?.route;
return route?.name || route?.path || null;
}
} catch (e) {
// 忽略错误
}
return null;
}
/**
* 统一的路由处理逻辑
*/
function handleRouteTracking({
location,
clientRoutes,
action,
basename,
isFirst,
}) {
// 延迟执行以等待页面标题更新 (例如等待 PageContainer 或 Helmet 生效)
setTimeout(() => {
try {
// 收集用户信息
const user = collectUserInfo();
if (!user) {
return;
}
// 收集客户端信息
const client = collectClientInfo();
// 获取路由名称
const routeName = getRouteName(clientRoutes, location.pathname);
// 收集应用信息
const app = collectAppInfo(location, document, routeName);
// 构建完整的埋点数据
const trackingData = {
client,
app,
user,
event: {
name: 'page_view',
type: 'navigation',
},
data: {
action: action || 'PUSH',
basename: basename || '',
is_first: isFirst || false,
route_path: location.pathname,
route_name: routeName,
route_search: location.search || '',
route_hash: location.hash || '',
route_state: location.state || null,
route_key: location.key || null,
timestamp: new Date().toISOString(),
},
};
// 发送埋点
createTracking(trackingData).catch((error) => {
// 静默处理错误
});
} catch (error) {
// 静默处理错误
}
}, 300);
}
// Umi 4 推荐使用 defineApp
export default defineApp({
onRouteChange: handleRouteTracking,
});
// 保留命名导出以兼容某些 Umi 配置
export const onRouteChange = handleRouteTracking;
import { login } from '@/services/DataService';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { LoginForm, ProFormText } from '@ant-design/pro-components';
import { Alert, Flex, Modal, Typography } from 'antd';
import { createStyles } from 'antd-style';
import { useState } from 'react';
const useStyle = createStyles(({ css }) => {
return {
modal: css`
.ant-modal-content {
border-radius: 30px;
padding: 0;
overflow: hidden;
background: radial-gradient(
circle at top right,
rgba(125, 211, 252, 0.22),
transparent 20%
),
linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
}
.ant-modal-body {
padding: 0;
}
`,
shell: css`
display: grid;
grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
min-height: 540px;
`,
visual: css`
padding: 38px 34px;
background: radial-gradient(
circle at top left,
rgba(0, 166, 232, 0.22),
transparent 22%
),
linear-gradient(160deg, #0f172a 0%, #1e293b 100%);
color: #f8fafc;
display: flex;
flex-direction: column;
justify-content: space-between;
`,
visualBadge: css`
width: fit-content;
padding: 8px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.14);
font-size: 12px;
letter-spacing: 0.04em;
`,
visualTitle: css`
margin-top: 24px;
h2 {
margin: 0;
font-size: 34px;
line-height: 1.15;
color: #f8fafc;
}
p {
margin: 14px 0 0;
font-size: 15px;
line-height: 1.8;
color: rgba(226, 232, 240, 0.78);
}
`,
visualList: css`
display: grid;
gap: 12px;
margin-top: 28px;
div {
border-radius: 18px;
padding: 14px 16px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.08);
}
strong {
display: block;
font-size: 14px;
color: #f8fafc;
}
span {
display: block;
margin-top: 4px;
font-size: 12px;
line-height: 1.6;
color: rgba(226, 232, 240, 0.72);
}
`,
formPane: css`
padding: 34px 32px;
display: flex;
align-items: center;
justify-content: center;
.ant-pro-form-login-container {
width: 100%;
box-shadow: none;
padding: 0;
}
.ant-pro-form-login-logo {
width: 58px;
height: 58px;
}
.ant-pro-form-login-title {
color: #0f172a;
font-size: 28px;
line-height: 1.2;
}
.ant-pro-form-login-subtitle {
color: #64748b;
font-size: 14px;
}
.ant-input-affix-wrapper,
.ant-input {
border-radius: 16px !important;
min-height: 48px;
}
.ant-btn-primary {
height: 46px;
border-radius: 16px;
font-weight: 600;
}
`,
formInner: css`
width: 100%;
max-width: 360px;
`,
error: css`
margin-bottom: 20px;
border-radius: 14px;
`,
};
});
export default (props) => {
const { onError, onSuccess } = props;
const [errorMessage, setErrorMessage] = useState(null);
const { styles } = useStyle();
return (
<Modal
{...props}
closable={false}
footer={null}
width={920}
className={styles.modal}
>
<div className={styles.shell}>
<div className={styles.visual}>
<div>
<div className={styles.visualBadge}>Enterprise AI Workspace</div>
<div className={styles.visualTitle}>
<Typography.Title level={2}>
让业务知识真正进入对话流程
</Typography.Title>
<Typography.Paragraph>
统一连接企业知识库、智能体工具链、文件理解与流程型操作,让每次提问都更接近可执行结果。
</Typography.Paragraph>
</div>
<div className={styles.visualList}>
<div>
<strong>知识与上下文统一</strong>
<span>支持话题问答、附件内容提取与历史会话连续理解。</span>
</div>
<div>
<strong>智能体可直接协作</strong>
<span>在同一工作台中触发专属助手和 MCP 工具调用。</span>
</div>
<div>
<strong>更适合内部业务落地</strong>
<span>保留稳定功能栈,同时提供更清晰一致的产品体验。</span>
</div>
</div>
</div>
</div>
<div className={styles.formPane}>
<div className={styles.formInner}>
<LoginForm
logo="https://oss.streams7.com/rsm/BfNsqGPGTKE2.svg"
title="智能助手"
subTitle="登录后继续使用统一 AI 对话工作台"
onFinish={async (form) => {
try {
const userInfo = await login(form.username, form.password);
localStorage.setItem(
'current_user',
JSON.stringify({
...userInfo,
token: undefined,
}),
);
setErrorMessage(null);
onSuccess?.(userInfo);
} catch (error) {
const nextErrorMessage =
error.response?.data?.message ||
error.message ||
'网络错误';
setErrorMessage(nextErrorMessage);
onError?.(nextErrorMessage, error);
}
}}
>
<Flex vertical>
<ProFormText
name="username"
fieldProps={{
size: 'large',
prefix: <UserOutlined className={'prefixIcon'} />,
}}
placeholder="用户名"
rules={[
{
required: true,
message: '请输入用户名',
},
]}
/>
<ProFormText.Password
name="password"
fieldProps={{
size: 'large',
prefix: <LockOutlined className={'prefixIcon'} />,
}}
placeholder="密码"
rules={[
{
required: true,
message: '请输入密码',
},
]}
/>
{errorMessage && (
<Alert
className={styles.error}
message={errorMessage}
type="error"
showIcon
/>
)}
</Flex>
</LoginForm>
</div>
</div>
</div>
</Modal>
);
};
export default (props) => {};
export const DEFAULT_NAME = 'Umi Max';
import { Outlet } from '@umijs/max';
import { App, ConfigProvider, theme } from 'antd';
import { createGlobalStyle } from 'antd-style';
const { defaultSeed, defaultAlgorithm } = theme;
const APP_FONT_FAMILY =
"'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif";
const GlobalStyle = createGlobalStyle`
:root {
--app-shell-bg:
radial-gradient(
circle at top left,
rgba(0, 156, 217, 0.16),
transparent 26%
),
radial-gradient(
circle at bottom right,
rgba(126, 34, 206, 0.08),
transparent 26%
),
linear-gradient(180deg, #f8fbff 0%, #eef4fb 100%);
--app-surface: rgba(255, 255, 255, 0.86);
--app-surface-strong: #ffffff;
--app-sidebar: #0f172a;
--app-sidebar-soft: rgba(30, 41, 59, 0.9);
--app-border: rgba(148, 163, 184, 0.2);
--app-shadow: 0 24px 60px rgba(15, 23, 42, 0.12);
--app-shadow-soft: 0 14px 32px rgba(15, 23, 42, 0.08);
--app-radius-xl: 32px;
--app-radius-lg: 24px;
--app-radius-md: 18px;
--app-radius-sm: 14px;
--app-content-width: 840px;
}
html,
body,
#root {
min-height: 100%;
}
body {
margin: 0;
background: var(--app-shell-bg);
color: #0f172a;
font-family: ${APP_FONT_FAMILY};
}
* {
box-sizing: border-box;
}
*::-webkit-scrollbar {
width: 10px;
height: 10px;
}
*::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.45);
border: 2px solid transparent;
border-radius: 999px;
background-clip: content-box;
}
*::-webkit-scrollbar-track {
background: transparent;
}
a {
color: inherit;
text-decoration: none;
}
.app-glass-card {
background: var(--app-surface);
border: 1px solid var(--app-border);
box-shadow: var(--app-shadow-soft);
backdrop-filter: blur(18px);
}
`;
export default () => {
const themeToken = defaultAlgorithm({
...defaultSeed,
colorPrimary: '#009cd9',
colorInfo: '#009cd9',
colorSuccess: '#1d9b62',
colorWarning: '#d97706',
colorError: '#dc2626',
colorBgBase: '#f6f9fc',
colorBgContainer: '#ffffff',
colorTextBase: '#0f172a',
borderRadius: 18,
borderRadiusLG: 24,
borderRadiusSM: 14,
boxShadow:
'0 20px 50px rgba(15, 23, 42, 0.12), 0 6px 18px rgba(15, 23, 42, 0.06)',
boxShadowSecondary:
'0 12px 30px rgba(15, 23, 42, 0.08), 0 4px 12px rgba(15, 23, 42, 0.05)',
controlHeight: 44,
fontSize: 15,
});
return (
<>
<GlobalStyle />
<ConfigProvider
theme={{
token: themeToken,
components: {
Layout: {
bodyBg: 'transparent',
siderBg: 'transparent',
},
Button: {
borderRadius: 14,
controlHeight: 42,
primaryShadow: 'none',
},
Input: {
borderRadius: 14,
activeShadow: '0 0 0 3px rgba(0, 156, 217, 0.12)',
},
Modal: {
borderRadiusLG: 24,
},
Collapse: {
headerBg: 'transparent',
contentBg: 'transparent',
},
Drawer: {
colorBgElevated: '#0f172a',
},
Steps: {
colorPrimary: '#009cd9',
},
},
}}
>
<App>
<Outlet />
</App>
</ConfigProvider>
</>
);
};
// 全局共享数据示例
import { DEFAULT_NAME } from '@/constants';
import { useState } from 'react';
const useUser = () => {
const [name, setName] = useState(DEFAULT_NAME);
return {
name,
setName,
};
};
export default useUser;
import { PageContainer } from '@ant-design/pro-components';
import { Access, useAccess } from '@umijs/max';
import { Button } from 'antd';
const AccessPage = () => {
const access = useAccess();
return (
<PageContainer
ghost
header={{
title: '权限示例',
}}
>
<Access accessible={access.canSeeAdmin}>
<Button>只有 Admin 可以看到这个按钮</Button>
</Access>
</PageContainer>
);
};
export default AccessPage;
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
.container {
padding-top: 80px;
}
This diff is collapsed.
This diff is collapsed.
// 示例方法,没有实际意义
export function trim(str) {
return str.trim();
}
/**
* 客户端信息收集工具
* 用于埋点功能,收集浏览器、操作系统、设备等信息
*/
function detectOS() {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
if (/windows/i.test(userAgent)) return 'Windows';
if (/macintosh|mac os x/i.test(userAgent)) return 'macOS';
if (/linux/i.test(userAgent)) return 'Linux';
if (/android/i.test(userAgent)) return 'Android';
if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) return 'iOS';
return 'Unknown';
}
function detectBrowser() {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
if (userAgent.indexOf('Firefox') > -1) return 'Firefox';
if (userAgent.indexOf('Opera') > -1 || userAgent.indexOf('OPR') > -1)
return 'Opera';
if (userAgent.indexOf('Chrome') > -1 && userAgent.indexOf('Edg') === -1)
return 'Chrome';
if (userAgent.indexOf('Safari') > -1 && userAgent.indexOf('Chrome') === -1)
return 'Safari';
if (userAgent.indexOf('Edg') > -1) return 'Edge';
return 'Unknown';
}
function detectDeviceType() {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
const width = window.screen.width || window.innerWidth;
if (
/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
userAgent,
)
) {
return width < 768 ? 'mobile' : 'tablet';
}
return width >= 1024 ? 'desktop' : width >= 768 ? 'tablet' : 'mobile';
}
function detectNetwork() {
try {
const connection =
navigator.connection ||
navigator.mozConnection ||
navigator.webkitConnection;
if (connection) {
return connection.type || connection.effectiveType || 'unknown';
}
return navigator.onLine ? 'online' : 'offline';
} catch (e) {
return 'unknown';
}
}
export function collectClientInfo() {
try {
return {
user_agent: navigator.userAgent || '',
os: detectOS(),
browser: detectBrowser(),
device_type: detectDeviceType(),
screen_width: window.screen.width || 0,
screen_height: window.screen.height || 0,
network: detectNetwork(),
app_version: '1.0.0',
language: navigator.language || 'unknown',
};
} catch (e) {
return {};
}
}
export function collectAppInfo(location, document, routeName = null) {
try {
let applicationTitle = '';
try {
// 尝试从 localStorage 获取
const current_user = JSON.parse(
localStorage.getItem('current_user') || 'null',
);
// 如果没有专门的 site_info,尝试从用户关联的应用信息中找
applicationTitle = document?.title || 'AI 应用';
} catch (e) {}
return {
url: window.location.href,
path: location.pathname,
title: document?.title || '',
application_title: applicationTitle,
referrer: document?.referrer || '',
route_name: routeName,
};
} catch (e) {
return { path: location.pathname };
}
}
export function collectUserInfo() {
try {
const userStr = localStorage.getItem('current_user');
if (!userStr || userStr === 'null') {
return null;
}
const currentUser = JSON.parse(userStr);
if (currentUser && currentUser._id) {
return {
_id: currentUser._id,
username: currentUser.username || '',
display_name: currentUser.display_name || '',
type: currentUser.type || 'normal',
};
}
// 如果只有 ID 字符串
if (typeof currentUser === 'string') {
return { _id: currentUser };
}
if (currentUser?._id) {
return { _id: currentUser._id };
}
} catch (e) {
// 忽略错误
}
return null;
}
{
"extends": "./src/.umi/tsconfig.json"
}
import '@umijs/max/typings';
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment