web端打通oss资源上传

This commit is contained in:
2026-07-01 21:46:36 +08:00
parent ef6cc18fb2
commit 139e37651b
22 changed files with 1186 additions and 40 deletions
+2
View File
@@ -17,6 +17,7 @@ import HqAccountsPage from './pages/HqAccountsPage';
import CitiesPage from './pages/CitiesPage';
import StoreMediaPage from './pages/StoreMediaPage';
import ProductsPage from './pages/ProductsPage';
import ResourcesPage from './pages/ResourcesPage';
function RequireAuth({ children }: { children: React.ReactNode }) {
if (!getToken()) return <Navigate to="/login" replace />;
@@ -41,6 +42,7 @@ export default function App() {
<Route path="/stores" element={<StoresPage />} />
<Route path="/store-accounts" element={<StoreAccountsPage />} />
<Route path="/store-media" element={<StoreMediaPage />} />
<Route path="/resources" element={<ResourcesPage />} />
<Route path="/partners" element={<PartnersPage />} />
<Route path="/cities" element={<CitiesPage />} />
<Route path="/partner-accounts" element={<PartnerAccountsPage />} />
@@ -0,0 +1,91 @@
import { useState } from 'react';
import { Button, Image, Input, Space, Upload, message } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
import type { UploadProps } from 'antd';
import { uploadFileToOss, type OssMediaType, type UploadFileResult } from '../lib/upload';
type OssUploadProps = {
value?: string;
onChange?: (url: string) => void;
onUploaded?: (result: UploadFileResult) => void;
bizType: string;
mediaType?: OssMediaType;
accept?: string;
maxSizeMb?: number;
placeholder?: string;
};
const DEFAULT_MAX_MB = 10;
export default function OssUpload({
value,
onChange,
onUploaded,
bizType,
mediaType = 'IMAGE',
accept,
maxSizeMb = DEFAULT_MAX_MB,
placeholder = '上传后自动填入,或手动粘贴 URL',
}: OssUploadProps) {
const [uploading, setUploading] = useState(false);
const resolvedAccept =
accept ?? (mediaType === 'VIDEO' ? 'video/*' : mediaType === 'FILE' ? undefined : 'image/*');
const customRequest: UploadProps['customRequest'] = async ({ file, onSuccess, onError }) => {
const raw = file as File;
if (raw.size > maxSizeMb * 1024 * 1024) {
const err = new Error(`文件不能超过 ${maxSizeMb}MB`);
message.error(err.message);
onError?.(err);
return;
}
setUploading(true);
try {
const result = await uploadFileToOss(raw, { bizType, mediaType });
onChange?.(result.url);
onUploaded?.(result);
if (result.mock) {
message.info('当前为 Mock OSS,已使用占位 URL');
} else {
message.success('上传成功');
}
onSuccess?.(result);
} catch (e) {
const err = e instanceof Error ? e : new Error('上传失败');
message.error(err.message);
onError?.(err);
} finally {
setUploading(false);
}
};
return (
<Space direction="vertical" style={{ width: '100%' }} size="small">
{value && mediaType === 'IMAGE' && (
<Image src={value} width={120} height={120} style={{ objectFit: 'cover', borderRadius: 4 }} />
)}
{value && mediaType === 'VIDEO' && (
<video src={value} controls style={{ maxWidth: '100%', maxHeight: 160, borderRadius: 4 }} />
)}
<Space wrap>
<Upload
accept={resolvedAccept}
showUploadList={false}
customRequest={customRequest}
disabled={uploading}
>
<Button icon={<UploadOutlined />} loading={uploading}>
{mediaType === 'VIDEO' ? '上传视频' : mediaType === 'FILE' ? '上传文件' : '上传图片'}
</Button>
</Upload>
</Space>
<Input
value={value}
placeholder={placeholder}
onChange={(e) => onChange?.(e.target.value)}
allowClear
/>
</Space>
);
}
@@ -12,6 +12,7 @@ import {
CarOutlined,
SafetyOutlined,
LogoutOutlined,
CloudUploadOutlined,
} from '@ant-design/icons';
import { clearAuth, request, type HqProfile } from '../lib/api';
@@ -22,6 +23,7 @@ const MENU_ITEMS: MenuProps['items'] = [
{ key: '/users', icon: <UserOutlined />, label: '用户' },
{ key: '/products', icon: <ShoppingOutlined />, label: '商品' },
{ key: '/orders', icon: <ShoppingOutlined />, label: '订单' },
{ key: '/resources', icon: <CloudUploadOutlined />, label: 'OSS 资源库' },
{
key: 'stores-group',
icon: <ShopOutlined />,
+27
View File
@@ -42,6 +42,33 @@ export const PARTNER_BILL_STATUS_LABELS: Record<string, string> = {
export const MEDIA_TYPE_LABELS: Record<string, string> = {
IMAGE: '图片',
VIDEO: '视频',
FILE: '文件',
};
export const RESOURCE_OWNER_TYPE_LABELS: Record<string, string> = {
PRODUCT: '商品',
STORE: '门店',
PARTNER: '合伙人',
USER: '用户',
ORDER: '订单',
HQ: '总部',
};
export const RESOURCE_BIZ_TYPE_LABELS: Record<string, string> = {
COVER: '封面',
ENV: '环境图',
CONTRACT: '合同',
CAROUSEL: '轮播',
DETAIL: '详情',
AVATAR: '头像',
QRCODE: '二维码',
SIGN_PHOTO: '签收照',
VIDEO: '视频',
};
export const RESOURCE_STATUS_LABELS: Record<string, string> = {
ACTIVE: '有效',
DELETED: '已删除',
};
export const PRODUCT_STATUS_LABELS: Record<string, string> = {
+49
View File
@@ -0,0 +1,49 @@
import { apiBase, CLIENT_APP, getToken, clearAuth } from './api';
export type OssMediaType = 'IMAGE' | 'VIDEO' | 'FILE';
export type UploadFileResult = {
url: string;
ossKey: string;
bucket: string;
mock: boolean;
};
/** 经 API 服务端转存 OSS,避免浏览器直传跨域 */
export async function uploadFileToOss(
file: File,
options: { bizType: string; mediaType?: OssMediaType },
): Promise<UploadFileResult> {
const mediaType = options.mediaType ?? (file.type.startsWith('video/') ? 'VIDEO' : 'IMAGE');
const formData = new FormData();
formData.append('file', file);
formData.append('bizType', options.bizType);
formData.append('mediaType', mediaType);
const headers: Record<string, string> = {
'X-Client-App': CLIENT_APP,
};
const token = getToken();
if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch(`${apiBase}/common/resources/upload`, {
method: 'POST',
headers,
body: formData,
});
const json = await res.json();
if (json.code === 401) {
clearAuth();
window.location.href = '/login';
throw new Error('未登录');
}
if (json.code !== 0) throw new Error(json.message || '上传失败');
const data = json.data as UploadFileResult;
return {
url: data.url,
ossKey: data.ossKey,
bucket: data.bucket,
mock: data.mock,
};
}
+7 -2
View File
@@ -6,6 +6,7 @@ import type { ColumnsType } from 'antd/es/table';
import { request, type Paginated } from '../lib/api';
import { AROMA_TYPE_LABELS, PRODUCT_STATUS_LABELS, fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList';
import OssUpload from '../components/OssUpload';
type Row = {
id: string;
@@ -114,7 +115,9 @@ export default function ProductsPage() {
<Select options={Object.entries(PRODUCT_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
</Form.Item>
<Form.Item name="sortOrder" label="排序"><InputNumber min={0} style={{ width: '100%' }} /></Form.Item>
<Form.Item name="coverUrl" label="封面 URL"><Input placeholder="https://..." /></Form.Item>
<Form.Item name="coverUrl" label="封面">
<OssUpload bizType="COVER" mediaType="IMAGE" />
</Form.Item>
</Form>
</>
)}
@@ -142,7 +145,9 @@ export default function ProductsPage() {
<Select options={Object.entries(PRODUCT_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
</Form.Item>
<Form.Item name="sortOrder" label="排序"><InputNumber min={0} style={{ width: '100%' }} /></Form.Item>
<Form.Item name="coverUrl" label="封面 URL"><Input placeholder="https://..." /></Form.Item>
<Form.Item name="coverUrl" label="封面">
<OssUpload bizType="COVER" mediaType="IMAGE" />
</Form.Item>
</Form>
</Modal>
</div>
+266
View File
@@ -0,0 +1,266 @@
import { useEffect, useState } from 'react';
import {
Button, Form, Image, Input, InputNumber, Modal, Popconfirm, Select, Space, Table, Tag, Typography, message,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { request, type Paginated } from '../lib/api';
import {
MEDIA_TYPE_LABELS,
RESOURCE_BIZ_TYPE_LABELS,
RESOURCE_OWNER_TYPE_LABELS,
RESOURCE_STATUS_LABELS,
fmtTime,
} from '../lib/constants';
import OssUpload from '../components/OssUpload';
type Row = {
id: string;
ownerType: string;
ownerId: string;
bizType: string;
mediaType: string;
ossBucket: string;
ossKey: string;
url: string;
fileName?: string | null;
sortOrder: number;
status: string;
createdAt: string;
};
export default function ResourcesPage() {
const [form] = Form.useForm();
const [createForm] = Form.useForm();
const [filters, setFilters] = useState<Record<string, string>>({});
const [data, setData] = useState<Paginated<Row> | null>(null);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [createOpen, setCreateOpen] = useState(false);
const [saving, setSaving] = useState(false);
async function reload(p = page, ps = pageSize, f = filters) {
setLoading(true);
try {
const qs = new URLSearchParams({ page: String(p), pageSize: String(ps) });
if (f.ownerType) qs.set('ownerType', f.ownerType);
if (f.ownerId) qs.set('ownerId', f.ownerId);
if (f.bizType) qs.set('bizType', f.bizType);
if (f.status) qs.set('status', f.status);
const res = await request<Paginated<Row>>(`/common/resources?${qs}`);
setData(res);
} finally {
setLoading(false);
}
}
useEffect(() => {
void reload();
}, []);
const columns: ColumnsType<Row> = [
{ title: 'ID', dataIndex: 'id', width: 80 },
{
title: '归属',
width: 140,
render: (_, row) => (
<span>
{RESOURCE_OWNER_TYPE_LABELS[row.ownerType] || row.ownerType} / {row.ownerId}
</span>
),
},
{ title: '用途', dataIndex: 'bizType', width: 90, render: (v) => RESOURCE_BIZ_TYPE_LABELS[v] || v },
{ title: '类型', dataIndex: 'mediaType', width: 70, render: (v) => MEDIA_TYPE_LABELS[v] || v },
{
title: '预览',
dataIndex: 'url',
width: 90,
render: (url, row) =>
row.mediaType === 'IMAGE' ? (
<Image src={url} width={56} height={40} style={{ objectFit: 'cover' }} />
) : (
<a href={url} target="_blank" rel="noreferrer">
</a>
),
},
{ title: 'URL', dataIndex: 'url', ellipsis: true },
{ title: 'OSS Key', dataIndex: 'ossKey', ellipsis: true, width: 160 },
{ title: '排序', dataIndex: 'sortOrder', width: 60 },
{
title: '状态',
dataIndex: 'status',
width: 80,
render: (s) => <Tag color={s === 'ACTIVE' ? 'green' : 'default'}>{RESOURCE_STATUS_LABELS[s] || s}</Tag>,
},
{ title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime },
{
title: '操作',
width: 80,
render: (_, row) => (
<Popconfirm
title="确认删除该资源?"
onConfirm={async () => {
await request(`/common/resources/${row.id}`, { method: 'DELETE' });
message.success('已删除');
void reload();
}}
>
<Button type="link" size="small" danger>
</Button>
</Popconfirm>
),
},
];
return (
<div>
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
<Typography.Title level={4} style={{ margin: 0 }}>
OSS
</Typography.Title>
<Button type="primary" onClick={() => setCreateOpen(true)}>
</Button>
</Space>
<Form
form={form}
layout="inline"
style={{ marginBottom: 16 }}
onFinish={(v) => {
setFilters(v);
setPage(1);
void reload(1, pageSize, v);
}}
>
<Form.Item name="ownerType" label="归属类型">
<Select
allowClear
style={{ width: 120 }}
options={Object.entries(RESOURCE_OWNER_TYPE_LABELS).map(([value, label]) => ({ value, label }))}
/>
</Form.Item>
<Form.Item name="ownerId" label="归属 ID">
<Input allowClear style={{ width: 120 }} />
</Form.Item>
<Form.Item name="bizType" label="用途">
<Select
allowClear
style={{ width: 110 }}
options={Object.entries(RESOURCE_BIZ_TYPE_LABELS).map(([value, label]) => ({ value, label }))}
/>
</Form.Item>
<Form.Item name="status" label="状态">
<Select
allowClear
style={{ width: 100 }}
options={Object.entries(RESOURCE_STATUS_LABELS).map(([value, label]) => ({ value, label }))}
/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
</Button>
</Form.Item>
</Form>
<Table
rowKey="id"
className="admin-table-nowrap"
loading={loading}
columns={columns}
dataSource={data?.items ?? []}
scroll={{ x: 1200 }}
pagination={{
current: page,
pageSize,
total: data?.total ?? 0,
showSizeChanger: true,
onChange: (p, ps) => {
setPage(p);
setPageSize(ps);
void reload(p, ps);
},
}}
/>
<Modal
title="登记 OSS 资源"
open={createOpen}
confirmLoading={saving}
onCancel={() => setCreateOpen(false)}
onOk={async () => {
const v = await createForm.validateFields();
setSaving(true);
try {
await request('/common/resources', {
method: 'POST',
body: JSON.stringify({
ownerType: v.ownerType,
ownerId: v.ownerId,
bizType: v.bizType,
mediaType: v.mediaType,
ossKey: v.ossKey,
url: v.url,
fileName: v.fileName,
sortOrder: v.sortOrder ?? 0,
}),
});
message.success('已登记');
setCreateOpen(false);
createForm.resetFields();
void reload();
} finally {
setSaving(false);
}
}}
width={520}
>
<Form form={createForm} layout="vertical" initialValues={{ mediaType: 'IMAGE', bizType: 'COVER', sortOrder: 0 }}>
<Form.Item name="ownerType" label="归属类型" rules={[{ required: true }]}>
<Select options={Object.entries(RESOURCE_OWNER_TYPE_LABELS).map(([value, label]) => ({ value, label }))} />
</Form.Item>
<Form.Item name="ownerId" label="归属 ID" rules={[{ required: true }]}>
<Input placeholder="商品/门店等业务 ID" />
</Form.Item>
<Form.Item name="bizType" label="用途" rules={[{ required: true }]}>
<Select options={Object.entries(RESOURCE_BIZ_TYPE_LABELS).map(([value, label]) => ({ value, label }))} />
</Form.Item>
<Form.Item name="mediaType" label="媒体类型" rules={[{ required: true }]}>
<Select
options={[
...Object.entries(MEDIA_TYPE_LABELS).map(([value, label]) => ({ value, label })),
{ value: 'FILE', label: '文件' },
]}
/>
</Form.Item>
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.mediaType !== cur.mediaType || prev.bizType !== cur.bizType}>
{({ getFieldValue }) => {
const mediaType = getFieldValue('mediaType') || 'IMAGE';
const bizType = getFieldValue('bizType') || 'COVER';
return (
<Form.Item name="url" label="资源" rules={[{ required: true }]}>
<OssUpload
bizType={bizType}
mediaType={mediaType}
onUploaded={(result) => {
createForm.setFieldsValue({ ossKey: result.ossKey, url: result.url });
}}
/>
</Form.Item>
);
}}
</Form.Item>
<Form.Item name="ossKey" label="OSS Key" rules={[{ required: true }]}>
<Input placeholder="上传后自动填入" />
</Form.Item>
<Form.Item name="fileName" label="文件名">
<Input />
</Form.Item>
<Form.Item name="sortOrder" label="排序">
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
</div>
);
}
+15 -2
View File
@@ -6,6 +6,7 @@ import type { ColumnsType } from 'antd/es/table';
import { request, type Paginated } from '../lib/api';
import { MEDIA_TYPE_LABELS, fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList';
import OssUpload from '../components/OssUpload';
type Row = {
id: string; mediaType: string; url: string; sortOrder: number; createdAt: string;
@@ -102,7 +103,13 @@ export default function StoreMediaPage() {
<Form.Item name="mediaType" label="类型" rules={[{ required: true }]} initialValue="IMAGE">
<Select options={Object.entries(MEDIA_TYPE_LABELS).map(([value, label]) => ({ value, label }))} />
</Form.Item>
<Form.Item name="url" label="资源 URL" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.mediaType !== cur.mediaType}>
{({ getFieldValue }) => (
<Form.Item name="url" label="资源" rules={[{ required: true }]}>
<OssUpload bizType="ENV" mediaType={getFieldValue('mediaType') || 'IMAGE'} />
</Form.Item>
)}
</Form.Item>
<Form.Item name="sortOrder" label="排序" initialValue={0}><InputNumber min={0} style={{ width: '100%' }} /></Form.Item>
</Form>
</Modal>
@@ -116,7 +123,13 @@ export default function StoreMediaPage() {
}}>
<Form form={editForm} layout="vertical">
<Form.Item name="mediaType" label="类型"><Select options={Object.entries(MEDIA_TYPE_LABELS).map(([value, label]) => ({ value, label }))} /></Form.Item>
<Form.Item name="url" label="资源 URL" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.mediaType !== cur.mediaType}>
{({ getFieldValue }) => (
<Form.Item name="url" label="资源" rules={[{ required: true }]}>
<OssUpload bizType="ENV" mediaType={getFieldValue('mediaType') || 'IMAGE'} />
</Form.Item>
)}
</Form.Item>
<Form.Item name="sortOrder" label="排序"><InputNumber min={0} style={{ width: '100%' }} /></Form.Item>
</Form>
</Modal>
+9 -4
View File
@@ -6,6 +6,7 @@ import type { ColumnsType } from 'antd/es/table';
import { request, type Paginated } from '../lib/api';
import { STORE_STATUS_LABELS, fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList';
import OssUpload from '../components/OssUpload';
type StoreRow = {
id: string;
@@ -128,16 +129,18 @@ export default function StoresPage() {
<Descriptions.Item label="ID">{String(detail.id)}</Descriptions.Item>
<Descriptions.Item label="地址">{String(detail.province)}{String(detail.cityName)}{String(detail.district)}{String(detail.address)}</Descriptions.Item>
<Descriptions.Item label="核销数">{String(detail.redeemCount ?? 0)}</Descriptions.Item>
{detail.coverUrl && (
{detail.coverUrl ? (
<Descriptions.Item label="封面">
<Image src={String(detail.coverUrl)} width={120} />
</Descriptions.Item>
)}
) : null}
</Descriptions>
<Form form={editForm} layout="vertical">
<Form.Item name="name" label="名称" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="phone" label="电话" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="coverUrl" label="封面图 URL"><Input /></Form.Item>
<Form.Item name="coverUrl" label="封面图">
<OssUpload bizType="COVER" mediaType="IMAGE" />
</Form.Item>
<Form.Item name="intro" label="介绍"><Input.TextArea rows={4} /></Form.Item>
<Form.Item name="district" label="区县"><Input /></Form.Item>
<Form.Item name="address" label="详细地址"><Input /></Form.Item>
@@ -164,7 +167,9 @@ export default function StoresPage() {
<Form.Item name="phone" label="门店电话" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="address" label="详细地址" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="district" label="区县"><Input /></Form.Item>
<Form.Item name="coverUrl" label="封面图 URL"><Input /></Form.Item>
<Form.Item name="coverUrl" label="封面图">
<OssUpload bizType="COVER" mediaType="IMAGE" />
</Form.Item>
<Form.Item name="intro" label="介绍"><Input.TextArea rows={3} /></Form.Item>
<Form.Item name="accountPhone" label="店长手机"><Input placeholder="默认同门店电话" /></Form.Item>
<Form.Item name="accountName" label="店长姓名"><Input placeholder="默认同门店名" /></Form.Item>
+1
View File
@@ -16,6 +16,7 @@
"db:seed": "pnpm --filter @dukang/api prisma:seed",
"db:sync-benefit": "pnpm --filter @dukang/api prisma:sync-benefit",
"db:validate": "pnpm --filter @dukang/api prisma:validate",
"oss:cors": "node scripts/configure-oss-cors.mjs",
"sync:stitch": "node scripts/sync-stitch.mjs"
},
"engines": {
+432
View File
@@ -225,6 +225,9 @@ importers:
'@prisma/client':
specifier: ^5.18.0
version: 5.22.0(prisma@5.22.0)
ali-oss:
specifier: ^6.23.0
version: 6.23.0
bullmq:
specifier: ^5.12.0
version: 5.79.2
@@ -253,9 +256,15 @@ importers:
'@nestjs/schematics':
specifier: ^10.1.0
version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3)
'@types/ali-oss':
specifier: ^6.23.3
version: 6.23.3
'@types/express':
specifier: ^4.17.21
version: 4.17.25
'@types/multer':
specifier: ^2.1.0
version: 2.1.0
'@types/node':
specifier: ^20.14.0
version: 20.19.43
@@ -972,6 +981,9 @@ packages:
'@tsconfig/node16@1.0.4':
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
'@types/ali-oss@6.23.3':
resolution: {integrity: sha512-huSf6njkqhSeR37OMJTGMCyUg2d9Zp2AioefhYuUMeh0vNM+WybctrBor7bO/RcCbLCQI6sHn6NtPBIWALYVJg==}
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@@ -1017,6 +1029,9 @@ packages:
'@types/mime@1.3.5':
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
'@types/multer@2.1.0':
resolution: {integrity: sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==}
'@types/node@20.19.43':
resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==}
@@ -1134,6 +1149,14 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
address@1.2.2:
resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==}
engines: {node: '>= 10.0.0'}
agentkeepalive@3.5.3:
resolution: {integrity: sha512-yqXL+k5rr8+ZRpOAntkaaRgWgE5o8ESAj5DyRmVTCSoZxXmqemb9Dd7T4i5UzwuERdLAJUy6XzR9zFVuf0kzkw==}
engines: {node: '>= 4.0.0'}
ajv-formats@2.1.1:
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
peerDependencies:
@@ -1161,6 +1184,10 @@ packages:
ajv@8.20.0:
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
ali-oss@6.23.0:
resolution: {integrity: sha512-FipRmyd16Pr/tEey/YaaQ/24Pc3HEpLM9S1DRakEuXlSLXNIJnu1oJtHM53eVYpvW3dXapSjrip3xylZUTIZVQ==}
engines: {node: '>=8'}
ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
@@ -1195,6 +1222,9 @@ packages:
react: '>=16.9.0'
react-dom: '>=16.9.0'
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
@@ -1239,6 +1269,9 @@ packages:
resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
bowser@1.9.4:
resolution: {integrity: sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==}
brace-expansion@1.1.15:
resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==}
@@ -1263,6 +1296,9 @@ packages:
buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
builtin-status-codes@3.0.0:
resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==}
bullmq@5.79.2:
resolution: {integrity: sha512-FebD+8XCZl/hnS1R4to24L4EAN70XSndKZO0776M36vGRk5MKVhKlNFM8/34zXLXKyYB4QaeIPFhVXSYYGTHpQ==}
engines: {node: '>=12.22.0'}
@@ -1428,6 +1464,9 @@ packages:
copy-to-clipboard@3.3.3:
resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
copy-to@2.0.1:
resolution: {integrity: sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
@@ -1458,6 +1497,9 @@ packages:
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
dateformat@2.2.0:
resolution: {integrity: sha512-GODcnWq3YGoTnygPfi02ygEiRxqUxpJwuRHjdhJYuxpcZmDq4rjBiXYmbCCzStxo176ixfLT6i4NPwQooRySnw==}
dayjs@1.11.21:
resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==}
@@ -1486,6 +1528,10 @@ packages:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
default-user-agent@1.0.0:
resolution: {integrity: sha512-bDF7bg6OSNcSwFWPu4zYKpVkJZQYVrAANMYB8bc9Szem1D0yKdm4sa/rOCs2aC9+2GMqQ7KnwtZRvDhmLF0dXw==}
engines: {node: '>= 0.10.0'}
defaults@1.0.4:
resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
@@ -1517,6 +1563,10 @@ packages:
resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==}
engines: {node: '>=0.3.1'}
digest-header@1.1.0:
resolution: {integrity: sha512-glXVh42vz40yZb9Cq2oMOt70FIoWiv+vxNvdKdU8CwjLad25qHM3trLxhl9bVjdr6WaslIXhWpn0NO8T/67Qjg==}
engines: {node: '>= 8.0.0'}
dotenv-expand@10.0.0:
resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==}
engines: {node: '>=12'}
@@ -1554,6 +1604,13 @@ packages:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
end-or-error@1.0.1:
resolution: {integrity: sha512-OclLMSug+k2A0JKuf494im25ANRBVW8qsjmwbgX7lQ8P82H21PQ1PWkoYwb9y5yMBS69BPlwtzdIFClo3+7kOQ==}
engines: {node: '>= 0.11.14'}
enhanced-resolve@5.24.1:
resolution: {integrity: sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw==}
engines: {node: '>=10.13.0'}
@@ -1632,6 +1689,10 @@ packages:
resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==}
engines: {node: '>= 0.10.0'}
extend-shallow@2.0.1:
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
engines: {node: '>=0.10.0'}
external-editor@3.1.0:
resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
engines: {node: '>=4'}
@@ -1678,6 +1739,9 @@ packages:
typescript: '>3.6.0'
webpack: ^5.11.0
formstream@1.5.2:
resolution: {integrity: sha512-NASf0lgxC1AyKNXQIrXTEYkiX99LhCEXTkiGObXAkpBui86a4u8FjH1o2bGb3PpqI3kafC+yw4zWeK6l6VHTgg==}
forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
@@ -1716,6 +1780,9 @@ packages:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
get-ready@1.0.0:
resolution: {integrity: sha512-mFXCZPJIlcYcth+N8267+mghfYN9h3EhsDa6JSnbA3Wrhh/XFpuowviFcsDeYZtKspQyWyJqfs4O6P8CHeTwzw==}
get-stream@8.0.1:
resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
engines: {node: '>=16'}
@@ -1766,10 +1833,17 @@ packages:
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
engines: {node: '>=16.17.0'}
humanize-ms@1.2.1:
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
@@ -1812,6 +1886,13 @@ packages:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
is-class-hotfix@0.0.6:
resolution: {integrity: sha512-0n+pzCC6ICtVr/WXnN2f03TK/3BfXY7me4cjCAqT8TYXEl0+JBRoqBo94JJHXcyDSLUeWbNX8Fvy5g5RJdAstQ==}
is-extendable@0.1.1:
resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
engines: {node: '>=0.10.0'}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
@@ -1836,13 +1917,22 @@ packages:
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
is-type-of@1.4.0:
resolution: {integrity: sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==}
is-unicode-supported@0.1.0:
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
engines: {node: '>=10'}
isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
isstream@0.1.2:
resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==}
iterare@1.2.1:
resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==}
engines: {node: '>=6'}
@@ -1854,6 +1944,9 @@ packages:
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
engines: {node: '>= 10.13.0'}
js-base64@2.6.4:
resolution: {integrity: sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -1899,6 +1992,9 @@ packages:
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
engines: {node: '>=12', npm: '>=6'}
jstoxml@2.2.9:
resolution: {integrity: sha512-OYWlK0j+roh+eyaMROlNbS5cd5R25Y+IUpdl7cNdB8HNrkgwQzIS7L9MegxOiWNBj9dQhA/yAxiMwCC5mwNoBw==}
jwa@1.4.2:
resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
@@ -2018,6 +2114,11 @@ packages:
engines: {node: '>=4'}
hasBin: true
mime@2.6.0:
resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
engines: {node: '>=4.0.0'}
hasBin: true
mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
@@ -2071,6 +2172,9 @@ packages:
resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
nanoid@3.3.15:
resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -2102,6 +2206,10 @@ packages:
resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==}
hasBin: true
node-hex@1.0.1:
resolution: {integrity: sha512-iwpZdvW6Umz12ICmu9IYPRxg0tOLGmU3Tq2tKetejCj3oZd7b2nUXwP3a7QA5M9glWy8wlPS1G3RwM/CdsUbdQ==}
engines: {node: '>=8.0.0'}
node-releases@2.0.50:
resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==}
engines: {node: '>=18'}
@@ -2126,6 +2234,9 @@ packages:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
onetime@5.1.2:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
@@ -2138,10 +2249,20 @@ packages:
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
engines: {node: '>=10'}
os-name@1.0.3:
resolution: {integrity: sha512-f5estLO2KN8vgtTRaILIgEGBoBrMnZ3JQ7W9TMZCnOIGwHe8TRGSpcagnWDo+Dfhd/z08k9Xe75hvciJJ8Qaew==}
engines: {node: '>=0.10.0'}
hasBin: true
os-tmpdir@1.0.2:
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
engines: {node: '>=0.10.0'}
osx-release@1.1.0:
resolution: {integrity: sha512-ixCMMwnVxyHFQLQnINhmIpWqXIfS2YOXchwQrk+OFzmo6nDjQ0E4KXAyyUh0T0MZgV4bUhkRrAbVqlE4yLVq4A==}
engines: {node: '>=0.10.0'}
hasBin: true
p-limit@5.0.0:
resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==}
engines: {node: '>=18'}
@@ -2192,6 +2313,9 @@ packages:
pathval@1.1.1:
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
pause-stream@0.0.11:
resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -2206,6 +2330,9 @@ packages:
pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
platform@1.3.6:
resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==}
pluralize@8.0.0:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'}
@@ -2223,10 +2350,16 @@ packages:
engines: {node: '>=16.13'}
hasBin: true
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
pump@3.0.4:
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -2500,6 +2633,9 @@ packages:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
@@ -2557,12 +2693,19 @@ packages:
rxjs@7.8.2:
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
sax@1.6.0:
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
engines: {node: '>=11.0.0'}
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
@@ -2577,6 +2720,13 @@ packages:
scroll-into-view-if-needed@3.1.0:
resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==}
sdk-base@2.0.1:
resolution: {integrity: sha512-eeG26wRwhtwYuKGCDM3LixCaxY27Pa/5lK4rLKhQa7HBjJ3U3Y+f81MMZQRsDw/8SC2Dao/83yJTXJ8aULuN8Q==}
semver@5.7.2:
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
hasBin: true
semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
@@ -2656,6 +2806,10 @@ packages:
standard-as-callback@2.1.0:
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
statuses@1.5.0:
resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
engines: {node: '>= 0.6'}
statuses@2.0.2:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
@@ -2663,6 +2817,13 @@ packages:
std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
stream-http@2.8.2:
resolution: {integrity: sha512-QllfrBhqF1DPcz46WxKTs6Mz1Bpc+8Qm6vbqOpVav5odAXwbyzwnEczoWqtxrsmlO+cJqtPrp/8gWKWjaKLLlA==}
stream-wormhole@1.1.0:
resolution: {integrity: sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==}
engines: {node: '>=4.0.0'}
streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
@@ -2678,6 +2839,9 @@ packages:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
@@ -2771,6 +2935,13 @@ packages:
engines: {node: '>=10'}
hasBin: true
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
throttle-debounce@5.0.2:
resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
engines: {node: '>=12.22'}
@@ -2793,6 +2964,9 @@ packages:
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
engines: {node: '>=0.6.0'}
to-arraybuffer@1.0.1:
resolution: {integrity: sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -2879,6 +3053,10 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
unescape@1.0.1:
resolution: {integrity: sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==}
engines: {node: '>=0.10.0'}
universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
@@ -2896,9 +3074,22 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
urllib@2.44.1:
resolution: {integrity: sha512-vreOVvFizoiIz5NK9IYMgUknkriHHBVccn2VFfJhgKz6O2qwm0SgjFk4OpXFRDXpdrTx8EzM1DB0/pejrqXwPA==}
engines: {node: '>= 0.10.0'}
peerDependencies:
proxy-agent: ^5.0.0
peerDependenciesMeta:
proxy-agent:
optional: true
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
utility@1.18.0:
resolution: {integrity: sha512-PYxZDA+6QtvRvm//++aGdmKG/cI07jNwbROz0Ql+VzFV1+Z0Dy55NI4zZ7RHc9KKpBePNFwoErqIuqQv/cjiTA==}
engines: {node: '>= 0.12.0'}
utils-merge@1.0.1:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
@@ -3016,6 +3207,10 @@ packages:
engines: {node: '>=8'}
hasBin: true
win-release@1.1.1:
resolution: {integrity: sha512-iCRnKVvGxOQdsKhcQId2PXV1vV3J/sDPXKA4Oe9+Eti2nb2ESEsYHRYls/UjoUW3bIc5ZDO8dTH50A/5iVN+bw==}
engines: {node: '>=0.10.0'}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
@@ -3028,6 +3223,17 @@ packages:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
xml2js@0.6.2:
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
engines: {node: '>=4.0.0'}
xmlbuilder@11.0.1:
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
engines: {node: '>=4.0'}
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
@@ -3725,6 +3931,8 @@ snapshots:
'@tsconfig/node16@1.0.4': {}
'@types/ali-oss@6.23.3': {}
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.29.7
@@ -3791,6 +3999,10 @@ snapshots:
'@types/mime@1.3.5': {}
'@types/multer@2.1.0':
dependencies:
'@types/express': 4.17.25
'@types/node@20.19.43':
dependencies:
undici-types: 6.21.0
@@ -3959,6 +4171,12 @@ snapshots:
acorn@8.17.0: {}
address@1.2.2: {}
agentkeepalive@3.5.3:
dependencies:
humanize-ms: 1.2.1
ajv-formats@2.1.1(ajv@8.12.0):
optionalDependencies:
ajv: 8.12.0
@@ -3997,6 +4215,37 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
ali-oss@6.23.0:
dependencies:
address: 1.2.2
agentkeepalive: 3.5.3
bowser: 1.9.4
copy-to: 2.0.1
dateformat: 2.2.0
debug: 4.4.3
destroy: 1.2.0
end-or-error: 1.0.1
get-ready: 1.0.0
humanize-ms: 1.2.1
is-type-of: 1.4.0
js-base64: 2.6.4
jstoxml: 2.2.9
lodash: 4.18.1
merge-descriptors: 1.0.3
mime: 2.6.0
platform: 1.3.6
pump: 3.0.4
qs: 6.14.2
sdk-base: 2.0.1
stream-http: 2.8.2
stream-wormhole: 1.1.0
urllib: 2.44.1
utility: 1.18.0
xml2js: 0.6.2
transitivePeerDependencies:
- proxy-agent
- supports-color
ansi-colors@4.1.3: {}
ansi-escapes@4.3.2:
@@ -4073,6 +4322,8 @@ snapshots:
- luxon
- moment
any-promise@1.3.0: {}
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
@@ -4121,6 +4372,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
bowser@1.9.4: {}
brace-expansion@1.1.15:
dependencies:
balanced-match: 1.0.2
@@ -4151,6 +4404,8 @@ snapshots:
base64-js: 1.5.1
ieee754: 1.2.1
builtin-status-codes@3.0.0: {}
bullmq@5.79.2:
dependencies:
cron-parser: 4.9.0
@@ -4311,6 +4566,8 @@ snapshots:
dependencies:
toggle-selection: 1.0.6
copy-to@2.0.1: {}
core-util-is@1.0.3: {}
cors@2.8.5:
@@ -4341,6 +4598,8 @@ snapshots:
csstype@3.2.3: {}
dateformat@2.2.0: {}
dayjs@1.11.21: {}
debug@2.6.9:
@@ -4357,6 +4616,10 @@ snapshots:
deepmerge@4.3.1: {}
default-user-agent@1.0.0:
dependencies:
os-name: 1.0.3
defaults@1.0.4:
dependencies:
clone: 1.0.4
@@ -4380,6 +4643,8 @@ snapshots:
diff@4.0.4: {}
digest-header@1.1.0: {}
dotenv-expand@10.0.0: {}
dotenv@16.4.5: {}
@@ -4410,6 +4675,12 @@ snapshots:
encodeurl@2.0.0: {}
end-of-stream@1.4.5:
dependencies:
once: 1.4.0
end-or-error@1.0.1: {}
enhanced-resolve@5.24.1:
dependencies:
graceful-fs: 4.2.11
@@ -4532,6 +4803,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
extend-shallow@2.0.1:
dependencies:
is-extendable: 0.1.1
external-editor@3.1.0:
dependencies:
chardet: 0.7.0
@@ -4599,6 +4874,13 @@ snapshots:
typescript: 5.7.2
webpack: 5.97.1
formstream@1.5.2:
dependencies:
destroy: 1.2.0
mime: 2.6.0
node-hex: 1.0.1
pause-stream: 0.0.11
forwarded@0.2.0: {}
fresh@0.5.2: {}
@@ -4638,6 +4920,8 @@ snapshots:
dunder-proto: 1.0.1
es-object-atoms: 1.1.2
get-ready@1.0.0: {}
get-stream@8.0.1: {}
glob-parent@5.1.2:
@@ -4683,10 +4967,18 @@ snapshots:
human-signals@5.0.0: {}
humanize-ms@1.2.1:
dependencies:
ms: 2.1.3
iconv-lite@0.4.24:
dependencies:
safer-buffer: 2.1.2
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
ieee754@1.2.1: {}
import-fresh@3.3.1:
@@ -4770,6 +5062,10 @@ snapshots:
dependencies:
binary-extensions: 2.3.0
is-class-hotfix@0.0.6: {}
is-extendable@0.1.1: {}
is-extglob@2.1.1: {}
is-fullwidth-code-point@3.0.0: {}
@@ -4784,10 +5080,20 @@ snapshots:
is-stream@3.0.0: {}
is-type-of@1.4.0:
dependencies:
core-util-is: 1.0.3
is-class-hotfix: 0.0.6
isstream: 0.1.2
is-unicode-supported@0.1.0: {}
isarray@1.0.0: {}
isexe@2.0.0: {}
isstream@0.1.2: {}
iterare@1.2.1: {}
jackspeak@3.4.3:
@@ -4802,6 +5108,8 @@ snapshots:
merge-stream: 2.0.0
supports-color: 8.1.1
js-base64@2.6.4: {}
js-tokens@4.0.0: {}
js-tokens@9.0.1: {}
@@ -4847,6 +5155,8 @@ snapshots:
ms: 2.1.3
semver: 7.8.5
jstoxml@2.2.9: {}
jwa@1.4.2:
dependencies:
buffer-equal-constant-time: 1.0.1
@@ -4944,6 +5254,8 @@ snapshots:
mime@1.6.0: {}
mime@2.6.0: {}
mimic-fn@2.1.0: {}
mimic-fn@4.0.0: {}
@@ -5005,6 +5317,12 @@ snapshots:
mute-stream@1.0.0: {}
mz@2.7.0:
dependencies:
any-promise: 1.3.0
object-assign: 4.1.1
thenify-all: 1.6.0
nanoid@3.3.15: {}
negotiator@0.6.3: {}
@@ -5026,6 +5344,8 @@ snapshots:
detect-libc: 2.1.2
optional: true
node-hex@1.0.1: {}
node-releases@2.0.50: {}
normalize-path@3.0.0: {}
@@ -5042,6 +5362,10 @@ snapshots:
dependencies:
ee-first: 1.1.1
once@1.4.0:
dependencies:
wrappy: 1.0.2
onetime@5.1.2:
dependencies:
mimic-fn: 2.1.0
@@ -5062,8 +5386,17 @@ snapshots:
strip-ansi: 6.0.1
wcwidth: 1.0.1
os-name@1.0.3:
dependencies:
osx-release: 1.1.0
win-release: 1.1.1
os-tmpdir@1.0.2: {}
osx-release@1.1.0:
dependencies:
minimist: 1.2.8
p-limit@5.0.0:
dependencies:
yocto-queue: 1.2.2
@@ -5104,6 +5437,10 @@ snapshots:
pathval@1.1.1: {}
pause-stream@0.0.11:
dependencies:
through: 2.3.8
picocolors@1.1.1: {}
picomatch@2.3.2: {}
@@ -5116,6 +5453,8 @@ snapshots:
mlly: 1.8.2
pathe: 2.0.3
platform@1.3.6: {}
pluralize@8.0.0: {}
postcss@8.5.15:
@@ -5136,11 +5475,18 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
process-nextick-args@2.0.1: {}
proxy-addr@2.0.7:
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
pump@3.0.4:
dependencies:
end-of-stream: 1.4.5
once: 1.4.0
punycode@2.3.1: {}
qs@6.14.2:
@@ -5502,6 +5848,16 @@ snapshots:
dependencies:
loose-envify: 1.4.0
readable-stream@2.3.8:
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
@@ -5576,10 +5932,14 @@ snapshots:
dependencies:
tslib: 2.8.1
safe-buffer@5.1.2: {}
safe-buffer@5.2.1: {}
safer-buffer@2.1.2: {}
sax@1.6.0: {}
scheduler@0.23.2:
dependencies:
loose-envify: 1.4.0
@@ -5601,6 +5961,12 @@ snapshots:
dependencies:
compute-scroll-into-view: 3.1.1
sdk-base@2.0.1:
dependencies:
get-ready: 1.0.0
semver@5.7.2: {}
semver@6.3.1: {}
semver@7.8.5: {}
@@ -5698,10 +6064,22 @@ snapshots:
standard-as-callback@2.1.0: {}
statuses@1.5.0: {}
statuses@2.0.2: {}
std-env@3.10.0: {}
stream-http@2.8.2:
dependencies:
builtin-status-codes: 3.0.0
inherits: 2.0.4
readable-stream: 2.3.8
to-arraybuffer: 1.0.1
xtend: 4.0.2
stream-wormhole@1.1.0: {}
streamsearch@1.1.0: {}
string-convert@0.2.1: {}
@@ -5718,6 +6096,10 @@ snapshots:
emoji-regex: 9.2.2
strip-ansi: 7.2.0
string_decoder@1.1.1:
dependencies:
safe-buffer: 5.1.2
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
@@ -5771,6 +6153,14 @@ snapshots:
commander: 2.20.3
source-map-support: 0.5.21
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
thenify@3.3.1:
dependencies:
any-promise: 1.3.0
throttle-debounce@5.0.2: {}
through@2.3.8: {}
@@ -5785,6 +6175,8 @@ snapshots:
dependencies:
os-tmpdir: 1.0.2
to-arraybuffer@1.0.1: {}
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -5861,6 +6253,10 @@ snapshots:
undici-types@6.21.0: {}
unescape@1.0.1:
dependencies:
extend-shallow: 2.0.1
universalify@2.0.1: {}
unpipe@1.0.0: {}
@@ -5875,8 +6271,31 @@ snapshots:
dependencies:
punycode: 2.3.1
urllib@2.44.1:
dependencies:
any-promise: 1.3.0
content-type: 1.0.5
default-user-agent: 1.0.0
digest-header: 1.1.0
ee-first: 1.1.1
formstream: 1.5.2
humanize-ms: 1.2.1
iconv-lite: 0.6.3
pump: 3.0.4
qs: 6.14.2
statuses: 1.5.0
utility: 1.18.0
util-deprecate@1.0.2: {}
utility@1.18.0:
dependencies:
copy-to: 2.0.1
escape-html: 1.0.3
mkdirp: 0.5.6
mz: 2.7.0
unescape: 1.0.1
utils-merge@1.0.1: {}
v8-compile-cache-lib@3.0.1: {}
@@ -6014,6 +6433,10 @@ snapshots:
siginfo: 2.0.0
stackback: 0.0.2
win-release@1.1.1:
dependencies:
semver: 5.7.2
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0
@@ -6032,6 +6455,15 @@ snapshots:
string-width: 5.1.2
strip-ansi: 7.2.0
wrappy@1.0.2: {}
xml2js@0.6.2:
dependencies:
sax: 1.6.0
xmlbuilder: 11.0.1
xmlbuilder@11.0.1: {}
xtend@4.0.2: {}
yallist@3.1.1: {}
+85
View File
@@ -0,0 +1,85 @@
/**
* 为 OSS Bucket 配置浏览器直传所需的 CORS 规则。
* 使用 server/dukang-api/.env 中的 OSS 凭证。
*
* 用法:pnpm oss:cors
* 可选环境变量 OSS_CORS_ORIGINS(逗号分隔),默认包含本地 H5 / admin 端口。
*/
import { readFileSync, existsSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
const __dirname = dirname(fileURLToPath(import.meta.url));
const apiRoot = resolve(__dirname, '../server/dukang-api');
const require = createRequire(resolve(apiRoot, 'package.json'));
const OSS = require('ali-oss');
const envPath = resolve(apiRoot, '.env');
function loadEnvFile(path: string) {
if (!existsSync(path)) return;
for (const line of readFileSync(path, 'utf8').split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq <= 0) continue;
const key = trimmed.slice(0, eq).trim();
let value = trimmed.slice(eq + 1).trim();
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
if (process.env[key] === undefined) process.env[key] = value;
}
}
loadEnvFile(envPath);
const {
OSS_ACCESS_KEY_ID: accessKeyId,
OSS_ACCESS_KEY_SECRET: accessKeySecret,
OSS_BUCKET: bucket,
OSS_REGION: region = 'oss-cn-hangzhou',
OSS_CORS_ORIGINS,
} = process.env;
if (!accessKeyId || !accessKeySecret || !bucket) {
console.error('请在 server/dukang-api/.env 配置 OSS_ACCESS_KEY_ID、OSS_ACCESS_KEY_SECRET、OSS_BUCKET');
process.exit(1);
}
const defaultOrigins = [
'http://localhost:5173',
'http://localhost:5174',
'http://localhost:5175',
'http://127.0.0.1:5173',
'http://127.0.0.1:5174',
'http://127.0.0.1:5175',
];
const allowedOrigin = (OSS_CORS_ORIGINS ?? defaultOrigins.join(','))
.split(',')
.map((s) => s.trim())
.filter(Boolean);
const client = new OSS({
region,
accessKeyId,
accessKeySecret,
bucket,
});
const rules = [
{
allowedOrigin,
allowedMethod: ['GET', 'POST', 'PUT', 'HEAD'],
allowedHeader: ['*'],
exposeHeader: ['ETag', 'x-oss-request-id'],
maxAgeSeconds: 600,
},
];
console.log(`配置 Bucket「${bucket}」CORS,允许来源:`);
for (const origin of allowedOrigin) console.log(` - ${origin}`);
await client.putBucketCORS(bucket, rules);
console.log('CORS 规则已写入。若 H5 端仍直传 OSS,请确认来源域名已包含在列表中。');
+7 -1
View File
@@ -23,7 +23,7 @@ WX_MCH_PRIVATE_KEY=
WX_API_V3_KEY=
WX_PAY_NOTIFY_URL=https://your-domain.com/api/v1/callbacks/wechat/pay
# 阿里云 OSSOSS_ENABLED=true 且下方密钥齐全时生效;否则 Mock 占位 URL)
# 阿里云 OSSali-oss@6.xOSS_ENABLED=true 且下方密钥齐全时生效;否则 Mock 占位 URL)
# RAM 用户需具备 PutObject 权限;可在控制台 Bucket 授权策略中为该 RAM UID 授予读写
# 文档:https://help.aliyun.com/zh/oss/user-guide/use-bucket-policy-to-grant-permission-to-access-oss/
OSS_ENABLED=false
@@ -31,6 +31,10 @@ OSS_ACCESS_KEY_ID=
OSS_ACCESS_KEY_SECRET=
OSS_BUCKET=
OSS_REGION=oss-cn-hangzhou
# 可选:自定义 Endpoint(如内网 oss-cn-hangzhou-internal.aliyuncs.com
OSS_ENDPOINT=
# 可选:开启 V4 签名(需同步调整前端 PostObject 字段,默认 false 使用 V1
OSS_AUTHORIZATION_V4=false
# 公网访问域名:自定义 CDN 或 https://{bucket}.{region}.aliyuncs.com
OSS_CDN_BASE=
# 直传对象前缀(默认 uploads)
@@ -39,3 +43,5 @@ OSS_UPLOAD_PREFIX=uploads
OSS_UPLOAD_EXPIRE_SECONDS=900
# 单文件大小上限(字节,默认 10MB)
OSS_MAX_UPLOAD_BYTES=10485760
# 浏览器直传 OSS 时需配置 Bucket CORS;运行 pnpm oss:cors 或控制台手动添加
# OSS_CORS_ORIGINS=http://localhost:5173,http://localhost:5174,http://localhost:5175,https://your-domain.com
+3
View File
@@ -24,6 +24,7 @@
"@nestjs/jwt": "^10.2.0",
"@nestjs/platform-express": "^10.4.0",
"@prisma/client": "^5.18.0",
"ali-oss": "^6.23.0",
"bullmq": "^5.12.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
@@ -35,7 +36,9 @@
"devDependencies": {
"@nestjs/cli": "^10.4.0",
"@nestjs/schematics": "^10.1.0",
"@types/ali-oss": "^6.23.3",
"@types/express": "^4.17.21",
"@types/multer": "^2.1.0",
"@types/node": "^20.14.0",
"prisma": "^5.18.0",
"ts-node": "^10.9.2",
@@ -0,0 +1,25 @@
import OSS = require('ali-oss');
export type AliyunOssClientConfig = {
accessKeyId: string;
accessKeySecret: string;
bucket: string;
region: string;
endpoint?: string;
authorizationV4?: boolean;
};
export function createAliyunOssClient(config: AliyunOssClientConfig): OSS {
return new OSS({
region: config.region,
accessKeyId: config.accessKeyId,
accessKeySecret: config.accessKeySecret,
bucket: config.bucket,
...(config.endpoint ? { endpoint: config.endpoint } : {}),
...(config.authorizationV4 ? { authorizationV4: true } : {}),
});
}
export function resolveOssUploadHost(bucket: string, region: string): string {
return `https://${bucket}.${region}.aliyuncs.com`;
}
@@ -1,7 +1,13 @@
import { createHmac } from 'crypto';
import { Injectable } from '@nestjs/common';
import { randomUUID } from 'crypto';
import type { IOssProvider, OssUploadTokenInput, OssUploadTokenResult } from './oss.interface';
import type {
IOssProvider,
OssPutObjectInput,
OssPutObjectResult,
OssUploadTokenInput,
OssUploadTokenResult,
} from './oss.interface';
import { buildOssObjectKey } from './oss.key.util';
import { createAliyunOssClient, resolveOssUploadHost } from './oss.aliyun.client';
const DEFAULT_EXPIRE_SECONDS = 15 * 60;
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
@@ -13,43 +19,62 @@ export class OssAliyunProvider implements IOssProvider {
private readonly bucket = process.env.OSS_BUCKET ?? '';
private readonly region = process.env.OSS_REGION ?? 'oss-cn-hangzhou';
private readonly cdnBase = process.env.OSS_CDN_BASE ?? '';
private readonly endpoint = process.env.OSS_ENDPOINT ?? '';
private readonly uploadPrefix = (process.env.OSS_UPLOAD_PREFIX ?? 'uploads').replace(/\/$/, '');
private readonly expireSeconds = Number(process.env.OSS_UPLOAD_EXPIRE_SECONDS ?? DEFAULT_EXPIRE_SECONDS);
private readonly maxUploadBytes = Number(process.env.OSS_MAX_UPLOAD_BYTES ?? DEFAULT_MAX_BYTES);
private readonly authorizationV4 = process.env.OSS_AUTHORIZATION_V4 === 'true';
private client: ReturnType<typeof createAliyunOssClient> | null = null;
isEnabled() {
return !!(this.accessKeyId && this.accessKeySecret && this.bucket);
}
buildPublicUrl(ossKey: string) {
const key = ossKey.replace(/^\//, '');
if (this.cdnBase) {
return `${this.cdnBase.replace(/\/$/, '')}/${key}`;
}
return `https://${this.bucket}.${this.region}.aliyuncs.com/${key}`;
}
getUploadToken(dto: OssUploadTokenInput): OssUploadTokenResult {
private getClient() {
if (!this.isEnabled()) {
throw new Error('OSS credentials are not configured');
}
if (!this.client) {
this.client = createAliyunOssClient({
accessKeyId: this.accessKeyId,
accessKeySecret: this.accessKeySecret,
bucket: this.bucket,
region: this.region,
endpoint: this.endpoint || undefined,
authorizationV4: this.authorizationV4,
});
}
return this.client;
}
const ext = dto.fileName.includes('.') ? dto.fileName.split('.').pop() : 'bin';
const dir = `${this.uploadPrefix}/${dto.bizType.toLowerCase()}/`;
const ossKey = `${dir}${Date.now()}-${randomUUID().slice(0, 8)}.${ext}`;
buildPublicUrl(ossKey: string) {
const key = ossKey.replace(/^\//, '');
const client = this.isEnabled() ? this.getClient() : null;
if (client) {
return client.generateObjectUrl(key, this.cdnBase || undefined);
}
if (this.cdnBase) {
return `${this.cdnBase.replace(/\/$/, '')}/${key}`;
}
return `${resolveOssUploadHost(this.bucket, this.region)}/${key}`;
}
getUploadToken(dto: OssUploadTokenInput): OssUploadTokenResult {
const client = this.getClient();
const ossKey = buildOssObjectKey(this.uploadPrefix, dto.bizType, dto.fileName);
const expireAt = new Date(Date.now() + this.expireSeconds * 1000);
const host = `https://${this.bucket}.${this.region}.aliyuncs.com`;
const host = resolveOssUploadHost(this.bucket, this.region);
const policy = {
expiration: expireAt.toISOString(),
conditions: [
['content-length-range', 0, this.maxUploadBytes],
['eq', '$bucket', this.bucket],
['starts-with', '$key', dir],
['starts-with', '$key', `${this.uploadPrefix}/${dto.bizType.toLowerCase()}/`],
],
};
const policyBase64 = Buffer.from(JSON.stringify(policy)).toString('base64');
const signature = createHmac('sha1', this.accessKeySecret).update(policyBase64).digest('base64');
const signed = client.calculatePostSignature(policy);
return {
bucket: this.bucket,
@@ -61,9 +86,24 @@ export class OssAliyunProvider implements IOssProvider {
mediaType: dto.mediaType,
bizType: dto.bizType,
host,
policy: policyBase64,
signature,
accessKeyId: this.accessKeyId,
policy: signed.policy,
signature: signed.Signature,
accessKeyId: signed.OSSAccessKeyId,
};
}
async putObject(input: OssPutObjectInput): Promise<OssPutObjectResult> {
const client = this.getClient();
const ossKey = buildOssObjectKey(this.uploadPrefix, input.bizType, input.fileName);
await client.put(ossKey, input.buffer, {
mime: input.mimeType || 'application/octet-stream',
});
return {
bucket: this.bucket,
region: this.region,
ossKey,
url: this.buildPublicUrl(ossKey),
mock: false,
};
}
}
@@ -23,8 +23,25 @@ export interface OssUploadTokenResult {
accessKeyId?: string;
}
export interface OssPutObjectInput {
bizType: string;
mediaType: string;
fileName: string;
buffer: Buffer;
mimeType?: string;
}
export interface OssPutObjectResult {
bucket: string;
region: string;
ossKey: string;
url: string;
mock: boolean;
}
export interface IOssProvider {
isEnabled(): boolean;
getUploadToken(input: OssUploadTokenInput): OssUploadTokenResult;
putObject(input: OssPutObjectInput): Promise<OssPutObjectResult>;
buildPublicUrl(ossKey: string): string;
}
@@ -0,0 +1,7 @@
import { randomUUID } from 'crypto';
export function buildOssObjectKey(uploadPrefix: string, bizType: string, fileName: string): string {
const ext = fileName.includes('.') ? fileName.split('.').pop() : 'bin';
const dir = `${uploadPrefix.replace(/\/$/, '')}/${bizType.toLowerCase()}/`;
return `${dir}${Date.now()}-${randomUUID().slice(0, 8)}.${ext}`;
}
@@ -1,6 +1,12 @@
import { Injectable } from '@nestjs/common';
import { randomUUID } from 'crypto';
import type { IOssProvider, OssUploadTokenInput, OssUploadTokenResult } from './oss.interface';
import type {
IOssProvider,
OssPutObjectInput,
OssPutObjectResult,
OssUploadTokenInput,
OssUploadTokenResult,
} from './oss.interface';
import { buildOssObjectKey } from './oss.key.util';
@Injectable()
export class OssMockProvider implements IOssProvider {
@@ -17,8 +23,7 @@ export class OssMockProvider implements IOssProvider {
}
getUploadToken(dto: OssUploadTokenInput): OssUploadTokenResult {
const ext = dto.fileName.includes('.') ? dto.fileName.split('.').pop() : 'bin';
const key = `uploads/${dto.bizType.toLowerCase()}/${Date.now()}-${randomUUID().slice(0, 8)}.${ext}`;
const key = buildOssObjectKey('uploads', dto.bizType, dto.fileName);
return {
bucket: this.bucket,
region: this.region,
@@ -30,4 +35,15 @@ export class OssMockProvider implements IOssProvider {
bizType: dto.bizType,
};
}
async putObject(input: OssPutObjectInput): Promise<OssPutObjectResult> {
const ossKey = buildOssObjectKey('uploads', input.bizType, input.fileName);
return {
bucket: this.bucket,
region: this.region,
ossKey,
url: this.buildPublicUrl(ossKey),
mock: true,
};
}
}
@@ -14,6 +14,16 @@ export class UploadTokenDto {
fileName: string;
}
export class UploadFileDto {
@IsString()
@IsNotEmpty()
bizType: string;
@IsString()
@IsIn(['IMAGE', 'VIDEO', 'FILE'])
mediaType: string;
}
export class RegisterResourceDto {
@IsString()
@IsNotEmpty()
@@ -1,8 +1,23 @@
import { Body, Controller, Delete, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Query,
UploadedFile,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { ResourceService } from './resource.service';
import { ResourceListQueryDto } from './dto/common-query.dto';
import { RegisterResourceDto, UpdateResourceDto, UploadTokenDto } from './dto/common-mutate.dto';
import { RegisterResourceDto, UpdateResourceDto, UploadFileDto, UploadTokenDto } from './dto/common-mutate.dto';
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
@Controller('common/resources')
@UseGuards(JwtAuthGuard)
@@ -14,6 +29,16 @@ export class ResourceController {
return this.service.getUploadToken(dto);
}
@Post('upload')
@UseInterceptors(
FileInterceptor('file', {
limits: { fileSize: Number(process.env.OSS_MAX_UPLOAD_BYTES ?? DEFAULT_MAX_BYTES) },
}),
)
upload(@UploadedFile() file: Express.Multer.File, @Body() dto: UploadFileDto) {
return this.service.uploadFile(file, dto);
}
@Post()
register(@Body() dto: RegisterResourceDto) {
return this.service.register(dto);
@@ -1,4 +1,4 @@
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import type { ResourceBizType, ResourceMediaType, ResourceOwnerType } from '@prisma/client';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../common/prisma/prisma.module';
@@ -6,7 +6,9 @@ import { serializeBigInt } from '../../common/decorators/current-user.decorator'
import { OSS_PROVIDER } from '../../integrations/integrations.constants';
import type { IOssProvider } from '../../integrations/oss/oss.interface';
import type { ResourceListQueryDto } from './dto/common-query.dto';
import type { RegisterResourceDto, UpdateResourceDto, UploadTokenDto } from './dto/common-mutate.dto';
import type { RegisterResourceDto, UpdateResourceDto, UploadFileDto, UploadTokenDto } from './dto/common-mutate.dto';
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
@Injectable()
export class ResourceService {
@@ -19,6 +21,23 @@ export class ResourceService {
return this.oss.getUploadToken(dto);
}
async uploadFile(file: Express.Multer.File | undefined, dto: UploadFileDto) {
if (!file) {
throw new BadRequestException('请选择要上传的文件');
}
const maxUploadBytes = Number(process.env.OSS_MAX_UPLOAD_BYTES ?? DEFAULT_MAX_BYTES);
if (file.size > maxUploadBytes) {
throw new BadRequestException(`文件不能超过 ${Math.floor(maxUploadBytes / 1024 / 1024)}MB`);
}
return this.oss.putObject({
bizType: dto.bizType,
mediaType: dto.mediaType,
fileName: file.originalname || 'upload.bin',
buffer: file.buffer,
mimeType: file.mimetype,
});
}
async register(dto: RegisterResourceDto) {
const resource = await this.prisma.commonResource.create({
data: {