feat: add Midjourney (#138)

* 🚧 stash

*  feat: add Midjourney

* 📝 doc: update readme
This commit is contained in:
Buer
2024-04-05 04:03:46 +08:00
committed by GitHub
parent 87bfecf3e9
commit c1fc32add7
42 changed files with 2479 additions and 84 deletions

View File

@@ -7,7 +7,7 @@
使用了以下开源项目作为我们项目的一部分:
- [Berry Free React Admin Template](https://github.com/codedthemes/berry-free-react-admin-template)
- [minimal-ui-kit](minimal-ui-kit)
- [minimal-ui-kit](https://github.com/minimal-ui-kit/material-kit-react)
## 许可证

View File

@@ -132,6 +132,13 @@ export const CHANNEL_OPTIONS = {
color: 'primary',
url: 'https://platform.lingyiwanwu.com/details'
},
34: {
key: 34,
text: 'Midjourney',
value: 34,
color: 'orange',
url: ''
},
24: {
key: 24,
text: 'Azure Speech',

View File

@@ -11,7 +11,8 @@ import {
IconUserScan,
IconActivity,
IconBrandTelegram,
IconReceipt2
IconReceipt2,
IconBrush
} from '@tabler/icons-react';
// constant
@@ -27,7 +28,8 @@ const icons = {
IconUserScan,
IconActivity,
IconBrandTelegram,
IconReceipt2
IconReceipt2,
IconBrush
};
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
@@ -96,6 +98,14 @@ const panel = {
icon: icons.IconGardenCart,
breadcrumbs: false
},
{
id: 'midjourney',
title: 'Midjourney',
type: 'item',
url: '/panel/midjourney',
icon: icons.IconBrush,
breadcrumbs: false
},
{
id: 'user',
title: '用户',

View File

@@ -16,6 +16,7 @@ const NotFoundView = Loadable(lazy(() => import('views/Error')));
const Analytics = Loadable(lazy(() => import('views/Analytics')));
const Telegram = Loadable(lazy(() => import('views/Telegram')));
const Pricing = Loadable(lazy(() => import('views/Pricing')));
const Midjourney = Loadable(lazy(() => import('views/Midjourney')));
// dashboard routing
const Dashboard = Loadable(lazy(() => import('views/Dashboard')));
@@ -81,6 +82,10 @@ const MainRoutes = {
{
path: 'pricing',
element: <Pricing />
},
{
path: 'midjourney',
element: <Midjourney />
}
]
};

View File

@@ -12,15 +12,7 @@ export default function componentStyleOverrides(theme) {
}
}
},
MuiMenuItem: {
styleOverrides: {
root: {
'&:hover': {
backgroundColor: theme.colors?.grey100
}
}
}
}, //MuiAutocomplete-popper MuiPopover-root
//MuiAutocomplete-popper MuiPopover-root
MuiAutocomplete: {
styleOverrides: {
popper: {
@@ -247,7 +239,7 @@ export default function componentStyleOverrides(theme) {
MuiTooltip: {
styleOverrides: {
tooltip: {
color: theme.paper,
color: theme.colors.paper,
background: theme.colors?.grey700
}
}
@@ -266,6 +258,9 @@ export default function componentStyleOverrides(theme) {
.apexcharts-menu {
background: ${theme.backgroundDefault} !important
}
.apexcharts-gridline, .apexcharts-xaxistooltip-background, .apexcharts-yaxistooltip-background {
stroke: ${theme.divider} !important;
}
`
}
};

View File

@@ -19,14 +19,14 @@ const Footer = () => {
{siteInfo.system_name} {process.env.REACT_APP_VERSION}{' '}
</Link>
{' '}
<Link href="https://github.com/songquanpeng" target="_blank">
JustSong
</Link>{' '}
构建
<Link href="https://github.com/MartialBE" target="_blank">
MartialBE
</Link>
修改源代码遵循
开发基于
<Link href="https://github.com/songquanpeng" target="_blank">
JustSong
</Link>{' '}
One API源代码遵循
<Link href="https://opensource.org/licenses/mit-license.php"> MIT 协议</Link>
</>
)}

View File

@@ -234,6 +234,33 @@ const typeConfig = {
test_model: 'yi-34b-chat-0205'
},
modelGroup: 'Lingyiwanwu'
},
34: {
input: {
models: [
'mj_imagine',
'mj_variation',
'mj_reroll',
'mj_blend',
'mj_modal',
'mj_zoom',
'mj_shorten',
'mj_high_variation',
'mj_low_variation',
'mj_pan',
'mj_inpaint',
'mj_custom_zoom',
'mj_describe',
'mj_upscale',
'swap_face'
]
},
prompt: {
key: '密钥填写midjourney-proxy的密钥如果没有设置密钥可以随便填',
base_url: '地址填写midjourney-proxy部署的地址',
test_model: ''
},
modelGroup: 'Midjourney'
}
};

View File

@@ -0,0 +1,174 @@
import PropTypes from 'prop-types';
import { useState } from 'react';
import {
TableRow,
TableCell,
Button,
Dialog,
DialogActions,
DialogContent,
ButtonGroup,
Popover,
MenuItem,
MenuList,
Tooltip
} from '@mui/material';
import { timestamp2string, copy } from 'utils/common';
import Label from 'ui-component/Label';
import { ACTION_TYPE, CODE_TYPE, STATUS_TYPE } from '../type/Type';
import { IconCaretDownFilled, IconCopy, IconDownload, IconExternalLink } from '@tabler/icons-react';
function renderType(types, type) {
const typeOption = types[type];
if (typeOption) {
return (
<Label variant="filled" color={typeOption.color}>
{' '}
{typeOption.text}{' '}
</Label>
);
} else {
return (
<Label variant="filled" color="error">
{' '}
未知{' '}
</Label>
);
}
}
async function downloadImage(url, filename) {
const response = await fetch(url);
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = blobUrl;
link.download = filename;
link.click();
URL.revokeObjectURL(blobUrl);
}
function TruncatedText(text) {
const truncatedText = text.length > 30 ? text.substring(0, 100) + '...' : text;
return (
<Tooltip
placement="top"
title={text}
onClick={() => {
copy(text, '');
}}
>
<span>{truncatedText}</span>
</Tooltip>
);
}
export default function LogTableRow({ item, userIsAdmin }) {
const [open, setOpen] = useState(false);
const [menuOpen, setMenuOpen] = useState(null);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const handleOpenMenu = (event) => {
setMenuOpen(event.currentTarget);
};
const handleCloseMenu = () => {
setMenuOpen(null);
};
return (
<>
<TableRow tabIndex={item.id}>
<TableCell>{item.mj_id}</TableCell>
<TableCell>{timestamp2string(item.submit_time / 1000)}</TableCell>
{userIsAdmin && <TableCell>{item.channel_id || ''}</TableCell>}
{userIsAdmin && <TableCell>{item.user_id || ''}</TableCell>}
<TableCell>{renderType(ACTION_TYPE, item.action)}</TableCell>
{userIsAdmin && <TableCell>{renderType(CODE_TYPE, item.code)}</TableCell>}
{userIsAdmin && <TableCell>{renderType(STATUS_TYPE, item.status)}</TableCell>}
<TableCell>{item.progress}</TableCell>
<TableCell>
{item.image_url == '' ? (
'无'
) : (
<ButtonGroup size="small" aria-label="split button">
<Button color="primary" onClick={handleClickOpen}>
显示
</Button>
<Button onClick={handleOpenMenu}>
<IconCaretDownFilled size={'16px'} />
</Button>
</ButtonGroup>
)}
</TableCell>
<TableCell>{TruncatedText(item.prompt)}</TableCell>
<TableCell>{TruncatedText(item.prompt_en)}</TableCell>
<TableCell>{TruncatedText(item.fail_reason)}</TableCell>
</TableRow>
<Dialog open={open} onClose={handleClose}>
<DialogContent>
<img src={item.image_url} alt="item" style={{ maxWidth: '100%', maxHeight: '100%' }} />
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
关闭
</Button>
</DialogActions>
</Dialog>
<Popover
open={!!menuOpen}
anchorEl={menuOpen}
onClose={handleCloseMenu}
anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
PaperProps={{
sx: { width: 140 }
}}
>
<MenuList>
<MenuItem
onClick={() => {
handleCloseMenu();
copy(item.image_url, '图片地址');
}}
>
<IconCopy style={{ marginRight: '16px' }} />
复制地址
</MenuItem>
<MenuItem
onClick={async () => {
handleCloseMenu();
await downloadImage(item.image_url, item.mj_id + '.png');
}}
>
<IconDownload style={{ marginRight: '16px' }} /> 下载图片{' '}
</MenuItem>
<MenuItem
onClick={() => {
handleCloseMenu();
}}
>
<IconExternalLink style={{ marginRight: '16px' }} /> 新窗口打开{' '}
</MenuItem>
</MenuList>
</Popover>
</>
);
}
LogTableRow.propTypes = {
item: PropTypes.object,
userIsAdmin: PropTypes.bool
};

View File

@@ -0,0 +1,113 @@
import PropTypes from 'prop-types';
import { useTheme } from '@mui/material/styles';
import { IconBroadcast, IconCalendarEvent } from '@tabler/icons-react';
import { InputAdornment, OutlinedInput, Stack, FormControl, InputLabel } from '@mui/material';
import { LocalizationProvider, DateTimePicker } from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import dayjs from 'dayjs';
require('dayjs/locale/zh-cn');
// ----------------------------------------------------------------------
export default function TableToolBar({ filterName, handleFilterName, userIsAdmin }) {
const theme = useTheme();
const grey500 = theme.palette.grey[500];
return (
<>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={{ xs: 3, sm: 2, md: 4 }} padding={'24px'} paddingBottom={'0px'}>
{userIsAdmin && (
<FormControl>
<InputLabel htmlFor="channel-channel_id-label">渠道ID</InputLabel>
<OutlinedInput
id="channel_id"
name="channel_id"
sx={{
minWidth: '100%'
}}
label="渠道ID"
value={filterName.channel_id}
onChange={handleFilterName}
placeholder="渠道ID"
startAdornment={
<InputAdornment position="start">
<IconBroadcast stroke={1.5} size="20px" color={grey500} />
</InputAdornment>
}
/>
</FormControl>
)}
<FormControl>
<InputLabel htmlFor="channel-mj_id-label">任务ID</InputLabel>
<OutlinedInput
id="mj_id"
name="mj_id"
sx={{
minWidth: '100%'
}}
label="任务ID"
value={filterName.mj_id}
onChange={handleFilterName}
placeholder="任务ID"
startAdornment={
<InputAdornment position="start">
<IconCalendarEvent stroke={1.5} size="20px" color={grey500} />
</InputAdornment>
}
/>
</FormControl>
<FormControl>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={'zh-cn'}>
<DateTimePicker
label="起始时间"
ampm={false}
name="start_timestamp"
value={filterName.start_timestamp === 0 ? null : dayjs.unix(filterName.start_timestamp / 1000)}
onChange={(value) => {
if (value === null) {
handleFilterName({ target: { name: 'start_timestamp', value: 0 } });
return;
}
handleFilterName({ target: { name: 'start_timestamp', value: value.unix() * 1000 } });
}}
slotProps={{
actionBar: {
actions: ['clear', 'today', 'accept']
}
}}
/>
</LocalizationProvider>
</FormControl>
<FormControl>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={'zh-cn'}>
<DateTimePicker
label="结束时间"
name="end_timestamp"
ampm={false}
value={filterName.end_timestamp === 0 ? null : dayjs.unix(filterName.end_timestamp / 1000)}
onChange={(value) => {
if (value === null) {
handleFilterName({ target: { name: 'end_timestamp', value: 0 } });
return;
}
handleFilterName({ target: { name: 'end_timestamp', value: value.unix() * 1000 } });
}}
slotProps={{
actionBar: {
actions: ['clear', 'today', 'accept']
}
}}
/>
</LocalizationProvider>
</FormControl>
</Stack>
</>
);
}
TableToolBar.propTypes = {
filterName: PropTypes.object,
handleFilterName: PropTypes.func,
userIsAdmin: PropTypes.bool
};

View File

@@ -0,0 +1,247 @@
import { useState, useEffect, useCallback } from 'react';
import { showError } from 'utils/common';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableContainer from '@mui/material/TableContainer';
import PerfectScrollbar from 'react-perfect-scrollbar';
import TablePagination from '@mui/material/TablePagination';
import LinearProgress from '@mui/material/LinearProgress';
import ButtonGroup from '@mui/material/ButtonGroup';
import Toolbar from '@mui/material/Toolbar';
import { Button, Card, Stack, Container, Typography, Box } from '@mui/material';
import LogTableRow from './component/TableRow';
import KeywordTableHead from 'ui-component/TableHead';
import TableToolBar from './component/TableToolBar';
import { API } from 'utils/api';
import { isAdmin } from 'utils/common';
import { ITEMS_PER_PAGE } from 'constants';
import { IconRefresh, IconSearch } from '@tabler/icons-react';
import dayjs from 'dayjs';
export default function Log() {
const originalKeyword = {
p: 0,
channel_id: '',
mj_id: '',
start_timestamp: 0,
end_timestamp: dayjs().unix() * 1000 + 3600
};
const [page, setPage] = useState(0);
const [order, setOrder] = useState('desc');
const [orderBy, setOrderBy] = useState('id');
const [rowsPerPage, setRowsPerPage] = useState(ITEMS_PER_PAGE);
const [listCount, setListCount] = useState(0);
const [searching, setSearching] = useState(false);
const [toolBarValue, setToolBarValue] = useState(originalKeyword);
const [searchKeyword, setSearchKeyword] = useState(originalKeyword);
const [refreshFlag, setRefreshFlag] = useState(false);
const [logs, setLogs] = useState([]);
const userIsAdmin = isAdmin();
const handleSort = (event, id) => {
const isAsc = orderBy === id && order === 'asc';
if (id !== '') {
setOrder(isAsc ? 'desc' : 'asc');
setOrderBy(id);
}
};
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
setPage(0);
setRowsPerPage(parseInt(event.target.value, 10));
};
const searchLogs = async () => {
setPage(0);
setSearchKeyword(toolBarValue);
};
const handleToolBarValue = (event) => {
setToolBarValue({ ...toolBarValue, [event.target.name]: event.target.value });
};
const fetchData = useCallback(
async (page, rowsPerPage, keyword, order, orderBy) => {
setSearching(true);
try {
if (orderBy) {
orderBy = order === 'desc' ? '-' + orderBy : orderBy;
}
const url = userIsAdmin ? '/api/mj/' : '/api/mj/self/';
if (!userIsAdmin) {
delete keyword.channel_id;
}
const res = await API.get(url, {
params: {
page: page + 1,
size: rowsPerPage,
order: orderBy,
...keyword
}
});
const { success, message, data } = res.data;
if (success) {
setListCount(data.total_count);
setLogs(data.data);
} else {
showError(message);
}
} catch (error) {
console.error(error);
}
setSearching(false);
},
[userIsAdmin]
);
// 处理刷新
const handleRefresh = async () => {
setOrderBy('id');
setOrder('desc');
setToolBarValue(originalKeyword);
setSearchKeyword(originalKeyword);
setRefreshFlag(!refreshFlag);
};
useEffect(() => {
fetchData(page, rowsPerPage, searchKeyword, order, orderBy);
}, [page, rowsPerPage, searchKeyword, order, orderBy, fetchData, refreshFlag]);
return (
<>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}>
<Typography variant="h4">Midjourney</Typography>
</Stack>
<Card>
<Box component="form" noValidate>
<TableToolBar filterName={toolBarValue} handleFilterName={handleToolBarValue} userIsAdmin={userIsAdmin} />
</Box>
<Toolbar
sx={{
textAlign: 'right',
height: 50,
display: 'flex',
justifyContent: 'space-between',
p: (theme) => theme.spacing(0, 1, 0, 3)
}}
>
<Container>
<ButtonGroup variant="outlined" aria-label="outlined small primary button group">
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
刷新/清除搜索条件
</Button>
<Button onClick={searchLogs} startIcon={<IconSearch width={'18px'} />}>
搜索
</Button>
</ButtonGroup>
</Container>
</Toolbar>
{searching && <LinearProgress />}
<PerfectScrollbar component="div">
<TableContainer sx={{ overflow: 'unset' }}>
<Table sx={{ minWidth: 800 }}>
<KeywordTableHead
order={order}
orderBy={orderBy}
onRequestSort={handleSort}
headLabel={[
{
id: 'mj_id',
label: '任务ID',
disableSort: false
},
{
id: 'submit_time',
label: '提交时间',
disableSort: false
},
{
id: 'channel_id',
label: '渠道',
disableSort: false,
hide: !userIsAdmin
},
{
id: 'user_id',
label: '用户',
disableSort: false,
hide: !userIsAdmin
},
{
id: 'action',
label: '类型',
disableSort: false
},
{
id: 'code',
label: '提交结果',
disableSort: false,
hide: !userIsAdmin
},
{
id: 'status',
label: '任务状态',
disableSort: false,
hide: !userIsAdmin
},
{
id: 'progress',
label: '进度',
disableSort: true
},
{
id: 'image_url',
label: '结果图片',
disableSort: true,
width: '120px'
},
{
id: 'prompt',
label: 'Prompt',
disableSort: true
},
{
id: 'prompt_en',
label: 'PromptEn',
disableSort: true
},
{
id: 'fail_reason',
label: '失败原因',
disableSort: true
}
]}
/>
<TableBody>
{logs.map((row, index) => (
<LogTableRow item={row} key={`${row.id}_${index}`} userIsAdmin={userIsAdmin} />
))}
</TableBody>
</Table>
</TableContainer>
</PerfectScrollbar>
<TablePagination
page={page}
component="div"
count={listCount}
rowsPerPage={rowsPerPage}
onPageChange={handleChangePage}
rowsPerPageOptions={[10, 25, 30]}
onRowsPerPageChange={handleChangeRowsPerPage}
showFirstButton
showLastButton
/>
</Card>
</>
);
}

View File

@@ -0,0 +1,33 @@
export const ACTION_TYPE = {
IMAGINE: { value: 'IMAGINE', text: '绘图', color: 'primary' },
UPSCALE: { value: 'UPSCALE', text: '放大', color: 'orange' },
VARIATION: { value: 'VARIATION', text: '变换', color: 'default' },
HIGH_VARIATION: { value: 'HIGH_VARIATION', text: '强变换', color: 'default' },
LOW_VARIATION: { value: 'LOW_VARIATION', text: '弱变换', color: 'default' },
PAN: { value: 'PAN', text: '平移', color: 'secondary' },
DESCRIBE: { value: 'DESCRIBE', text: '图生文', color: 'secondary' },
BLEND: { value: 'BLEND', text: '图混合', color: 'secondary' },
SHORTEN: { value: 'SHORTEN', text: '缩词', color: 'secondary' },
REROLL: { value: 'REROLL', text: '重绘', color: 'secondary' },
INPAINT: { value: 'INPAINT', text: '局部重绘-提交', color: 'secondary' },
ZOOM: { value: 'ZOOM', text: '变焦', color: 'secondary' },
CUSTOM_ZOOM: { value: 'CUSTOM_ZOOM', text: '自定义变焦-提交', color: 'secondary' },
MODAL: { value: 'MODAL', text: '窗口处理', color: 'secondary' },
SWAP_FACE: { value: 'SWAP_FACE', text: '换脸', color: 'secondary' }
};
export const CODE_TYPE = {
1: { value: 1, text: '已提交', color: 'primary' },
21: { value: 21, text: '等待中', color: 'orange' },
22: { value: 22, text: '重复提交', color: 'default' },
0: { value: 0, text: '未提交', color: 'default' }
};
export const STATUS_TYPE = {
SUCCESS: { value: 'SUCCESS', text: '成功', color: 'success' },
NOT_START: { value: 'NOT_START', text: '未启动', color: 'default' },
SUBMITTED: { value: 'SUBMITTED', text: '队列中', color: 'secondary' },
IN_PROGRESS: { value: 'IN_PROGRESS', text: '执行中', color: 'primary' },
FAILURE: { value: 'FAILURE', text: '失败', color: 'orange' },
MODAL: { value: 'MODAL', text: '窗口等待', color: 'default' }
};

View File

@@ -29,7 +29,8 @@ const OperationSetting = () => {
DisplayTokenStatEnabled: '',
ApproximateTokenEnabled: '',
RetryTimes: 0,
RetryCooldownSeconds: 0
RetryCooldownSeconds: 0,
MjNotifyEnabled: ''
});
const [originInputs, setOriginInputs] = useState({});
let [loading, setLoading] = useState(false);
@@ -278,6 +279,22 @@ const OperationSetting = () => {
</Button>
</Stack>
</SubCard>
<SubCard title="其他设置">
<Stack justifyContent="flex-start" alignItems="flex-start" spacing={2}>
<Stack
direction={{ sm: 'column', md: 'row' }}
spacing={{ xs: 3, sm: 2, md: 4 }}
justifyContent="flex-start"
alignItems="flex-start"
>
<FormControlLabel
sx={{ marginLeft: '0px' }}
label="Midjourney 允许回调会泄露服务器ip地址"
control={<Checkbox checked={inputs.MjNotifyEnabled === 'true'} onChange={handleInputChange} name="MjNotifyEnabled" />}
/>
</Stack>
</Stack>
</SubCard>
<SubCard title="日志设置">
<Stack direction="column" justifyContent="flex-start" alignItems="flex-start" spacing={2}>
<FormControlLabel