@lobehub/chat 1.39.3 → 1.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +19 -8
- package/.eslintignore +1 -1
- package/CHANGELOG.md +33 -0
- package/changelog/v1.json +12 -0
- package/docs/.cdn.cache.json +25 -0
- package/docs/changelog/2023-09-09-plugin-system.mdx +1 -1
- package/docs/changelog/2023-09-09-plugin-system.zh-CN.mdx +1 -1
- package/docs/changelog/2024-09-20-artifacts.mdx +1 -1
- package/docs/changelog/2024-09-20-artifacts.zh-CN.mdx +1 -1
- package/docs/changelog/2024-10-27-pin-assistant.mdx +2 -2
- package/docs/changelog/2024-10-27-pin-assistant.zh-CN.mdx +2 -2
- package/docs/changelog/2024-11-06-share-text-json.mdx +2 -2
- package/docs/changelog/2024-11-06-share-text-json.zh-CN.mdx +2 -2
- package/docs/changelog/index.json +16 -16
- package/locales/ar/changelog.json +18 -0
- package/locales/ar/common.json +1 -0
- package/locales/ar/metadata.json +4 -0
- package/locales/bg-BG/changelog.json +18 -0
- package/locales/bg-BG/common.json +1 -0
- package/locales/bg-BG/metadata.json +4 -0
- package/locales/de-DE/changelog.json +18 -0
- package/locales/de-DE/common.json +1 -0
- package/locales/de-DE/metadata.json +4 -0
- package/locales/en-US/changelog.json +18 -0
- package/locales/en-US/common.json +1 -0
- package/locales/en-US/metadata.json +4 -0
- package/locales/es-ES/changelog.json +18 -0
- package/locales/es-ES/common.json +1 -0
- package/locales/es-ES/metadata.json +4 -0
- package/locales/fa-IR/changelog.json +18 -0
- package/locales/fa-IR/common.json +1 -0
- package/locales/fa-IR/metadata.json +4 -0
- package/locales/fr-FR/changelog.json +18 -0
- package/locales/fr-FR/common.json +1 -0
- package/locales/fr-FR/metadata.json +4 -0
- package/locales/it-IT/changelog.json +18 -0
- package/locales/it-IT/common.json +1 -0
- package/locales/it-IT/metadata.json +4 -0
- package/locales/ja-JP/changelog.json +18 -0
- package/locales/ja-JP/common.json +1 -0
- package/locales/ja-JP/metadata.json +4 -0
- package/locales/ko-KR/changelog.json +18 -0
- package/locales/ko-KR/common.json +1 -0
- package/locales/ko-KR/metadata.json +4 -0
- package/locales/nl-NL/changelog.json +18 -0
- package/locales/nl-NL/common.json +1 -0
- package/locales/nl-NL/metadata.json +4 -0
- package/locales/pl-PL/changelog.json +18 -0
- package/locales/pl-PL/common.json +1 -0
- package/locales/pl-PL/metadata.json +4 -0
- package/locales/pt-BR/changelog.json +18 -0
- package/locales/pt-BR/common.json +1 -0
- package/locales/pt-BR/metadata.json +4 -0
- package/locales/ru-RU/changelog.json +18 -0
- package/locales/ru-RU/common.json +1 -0
- package/locales/ru-RU/metadata.json +4 -0
- package/locales/tr-TR/changelog.json +18 -0
- package/locales/tr-TR/common.json +1 -0
- package/locales/tr-TR/metadata.json +4 -0
- package/locales/vi-VN/changelog.json +18 -0
- package/locales/vi-VN/common.json +1 -0
- package/locales/vi-VN/metadata.json +4 -0
- package/locales/zh-CN/changelog.json +18 -0
- package/locales/zh-CN/common.json +1 -0
- package/locales/zh-CN/metadata.json +4 -0
- package/locales/zh-TW/changelog.json +18 -0
- package/locales/zh-TW/common.json +1 -0
- package/locales/zh-TW/metadata.json +4 -0
- package/package.json +6 -1
- package/scripts/cdnWorkflow/index.ts +217 -0
- package/scripts/cdnWorkflow/optimized.ts +21 -0
- package/scripts/cdnWorkflow/s3/index.ts +120 -0
- package/scripts/cdnWorkflow/s3/types.ts +25 -0
- package/scripts/cdnWorkflow/s3/utils.ts +106 -0
- package/scripts/cdnWorkflow/uploader.ts +73 -0
- package/scripts/cdnWorkflow/utils.ts +93 -0
- package/src/app/(main)/(mobile)/me/(home)/__tests__/useCategory.test.tsx +25 -12
- package/src/app/(main)/(mobile)/me/(home)/features/useCategory.tsx +19 -9
- package/src/app/(main)/_layout/Desktop.tsx +4 -1
- package/src/app/(main)/_layout/Mobile.tsx +2 -1
- package/src/app/(main)/changelog/_layout/Desktop.tsx +25 -0
- package/src/app/(main)/changelog/_layout/Mobile/Header.tsx +33 -0
- package/src/app/(main)/changelog/_layout/Mobile/index.tsx +21 -0
- package/src/app/(main)/changelog/error.tsx +5 -0
- package/src/app/(main)/changelog/features/GridLayout.tsx +22 -0
- package/src/app/(main)/changelog/features/Hero.tsx +40 -0
- package/src/app/(main)/changelog/features/Post.tsx +56 -0
- package/src/app/(main)/changelog/features/PublishedTime.tsx +50 -0
- package/src/app/(main)/changelog/features/VersionTag.tsx +27 -0
- package/src/app/(main)/changelog/layout.tsx +10 -0
- package/src/app/(main)/changelog/loading.tsx +3 -0
- package/src/app/(main)/changelog/modal/page.tsx +23 -0
- package/src/app/(main)/changelog/not-found.tsx +3 -0
- package/src/app/(main)/changelog/page.tsx +73 -0
- package/src/app/(main)/chat/(workspace)/page.tsx +9 -2
- package/src/app/(main)/settings/about/features/Version.tsx +2 -2
- package/src/app/@modal/(.)changelog/modal/features/Cover.tsx +48 -0
- package/src/app/@modal/(.)changelog/modal/features/Hero.tsx +29 -0
- package/src/app/@modal/(.)changelog/modal/features/Pagination.tsx +54 -0
- package/src/app/@modal/(.)changelog/modal/features/Post.tsx +57 -0
- package/src/app/@modal/(.)changelog/modal/features/PublishedTime.tsx +50 -0
- package/src/app/@modal/(.)changelog/modal/features/ReadDetail.tsx +94 -0
- package/src/app/@modal/(.)changelog/modal/features/UpdateChangelogStatus.tsx +21 -0
- package/src/app/@modal/(.)changelog/modal/features/VersionTag.tsx +27 -0
- package/src/app/@modal/(.)changelog/modal/layout.tsx +39 -0
- package/src/app/@modal/(.)changelog/modal/loading.tsx +10 -0
- package/src/app/@modal/(.)changelog/modal/page.tsx +37 -0
- package/src/app/@modal/(.)settings/modal/layout.tsx +19 -16
- package/src/app/@modal/_layout/ModalLayout.tsx +63 -0
- package/src/app/@modal/chat/(.)settings/modal/layout.tsx +20 -17
- package/src/app/@modal/layout.tsx +5 -69
- package/src/components/mdx/Image.tsx +50 -0
- package/src/components/mdx/index.tsx +2 -0
- package/src/const/url.ts +1 -0
- package/src/features/ChangelogModal/index.tsx +22 -0
- package/src/features/User/UserPanel/useMenu.tsx +50 -46
- package/src/features/User/__tests__/useMenu.test.tsx +7 -6
- package/src/hooks/useInterceptingRoutes.ts +1 -6
- package/src/hooks/useShare.tsx +1 -0
- package/src/locales/default/changelog.ts +18 -0
- package/src/locales/default/common.ts +1 -0
- package/src/locales/default/index.ts +2 -0
- package/src/locales/default/metadata.ts +4 -0
- package/src/server/metadata.ts +5 -3
- package/src/server/routers/edge/appStatus.ts +3 -0
- package/src/server/routers/edge/index.ts +2 -0
- package/src/server/routers/lambda/agent.ts +1 -1
- package/src/server/services/changelog/index.test.ts +310 -0
- package/src/server/services/changelog/index.ts +196 -0
- package/src/server/services/discover/index.test.ts +0 -1
- package/src/server/sitemap.ts +4 -1
- package/src/services/__tests__/chat.test.ts +1 -1
- package/src/services/__tests__/global.test.ts +5 -2
- package/src/services/_auth.ts +1 -1
- package/src/services/agent.ts +25 -21
- package/src/services/chat.ts +2 -2
- package/src/services/file/ClientS3/index.ts +6 -6
- package/src/services/file/client.ts +14 -15
- package/src/services/file/server.ts +20 -25
- package/src/services/global.ts +2 -2
- package/src/services/import/client.ts +6 -5
- package/src/services/import/server.ts +6 -5
- package/src/services/import/type.ts +7 -0
- package/src/services/knowledgeBase.ts +19 -19
- package/src/services/message/_deprecated.ts +5 -0
- package/src/services/message/client.ts +52 -48
- package/src/services/message/server.ts +50 -53
- package/src/services/message/type.ts +2 -2
- package/src/services/plugin/client.ts +16 -22
- package/src/services/plugin/server.ts +15 -19
- package/src/services/rag.ts +18 -18
- package/src/services/ragEval.ts +29 -26
- package/src/services/session/_deprecated.ts +2 -2
- package/src/services/session/client.ts +55 -81
- package/src/services/session/server.ts +50 -74
- package/src/services/session/type.ts +4 -6
- package/src/services/share.ts +4 -4
- package/src/services/textToImage.ts +5 -2
- package/src/services/thread/client.ts +9 -15
- package/src/services/thread/server.ts +10 -15
- package/src/services/topic/client.ts +25 -25
- package/src/services/topic/server.ts +25 -42
- package/src/services/trace.ts +4 -4
- package/src/services/user/client.ts +13 -17
- package/src/services/user/server.ts +9 -13
- package/src/services/user/type.ts +1 -1
- package/src/store/chat/slices/message/reducer.ts +3 -2
- package/src/store/global/action.ts +27 -22
- package/src/store/global/initialState.ts +1 -0
- package/src/types/changelog.ts +6 -0
- package/src/types/message/index.ts +10 -8
- package/src/app/@modal/features/InterceptingContext.tsx +0 -9
@@ -0,0 +1,217 @@
|
|
1
|
+
import { consola } from 'consola';
|
2
|
+
import { writeJSONSync } from 'fs-extra';
|
3
|
+
import matter from 'gray-matter';
|
4
|
+
import { createHash } from 'node:crypto';
|
5
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
6
|
+
import { resolve } from 'node:path';
|
7
|
+
import pMap from 'p-map';
|
8
|
+
|
9
|
+
import { uploader } from './uploader';
|
10
|
+
import {
|
11
|
+
changelogIndex,
|
12
|
+
changelogIndexPath,
|
13
|
+
extractHttpsLinks,
|
14
|
+
fetchImageAsFile,
|
15
|
+
mergeAndDeduplicateArrays,
|
16
|
+
posts,
|
17
|
+
root,
|
18
|
+
} from './utils';
|
19
|
+
|
20
|
+
// 定义常量
|
21
|
+
const GITHUB_CDN = 'https://github.com/lobehub/lobe-chat/assets/';
|
22
|
+
const CHECK_CDN = [
|
23
|
+
'https://cdn.nlark.com/yuque/0/',
|
24
|
+
'https://s.imtccdn.com/',
|
25
|
+
'https://oss.home.imtc.top/',
|
26
|
+
'https://www.anthropic.com/_next/image',
|
27
|
+
'https://miro.medium.com/v2/',
|
28
|
+
'https://images.unsplash.com/',
|
29
|
+
'https://github.com/user-attachments/assets',
|
30
|
+
];
|
31
|
+
|
32
|
+
const CACHE_FILE = resolve(root, 'docs', '.cdn.cache.json');
|
33
|
+
|
34
|
+
class ImageCDNUploader {
|
35
|
+
private cache: { [link: string]: string } = {};
|
36
|
+
|
37
|
+
constructor() {
|
38
|
+
this.loadCache();
|
39
|
+
}
|
40
|
+
|
41
|
+
// 从文件加载缓存数据
|
42
|
+
private loadCache() {
|
43
|
+
try {
|
44
|
+
this.cache = JSON.parse(readFileSync(CACHE_FILE, 'utf8'));
|
45
|
+
} catch (error) {
|
46
|
+
consola.error('Failed to load cache', error);
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
// 将缓存数据写入文件
|
51
|
+
private writeCache() {
|
52
|
+
try {
|
53
|
+
writeFileSync(CACHE_FILE, JSON.stringify(this.cache, null, 2));
|
54
|
+
} catch (error) {
|
55
|
+
consola.error('Failed to write cache', error);
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
59
|
+
// 收集所有的图片链接
|
60
|
+
private collectImageLinks(): string[] {
|
61
|
+
const links: string[][] = posts.map((post) => {
|
62
|
+
const mdx = readFileSync(post, 'utf8');
|
63
|
+
const { content, data } = matter(mdx);
|
64
|
+
let inlineLinks: string[] = extractHttpsLinks(content);
|
65
|
+
|
66
|
+
// 添加特定字段中的图片链接
|
67
|
+
if (data?.image) inlineLinks.push(data.image);
|
68
|
+
if (data?.seo?.image) inlineLinks.push(data.seo.image);
|
69
|
+
|
70
|
+
// 过滤出有效的 CDN 链接
|
71
|
+
return inlineLinks.filter(
|
72
|
+
(link) =>
|
73
|
+
(link.startsWith(GITHUB_CDN) || CHECK_CDN.some((cdn) => link.startsWith(cdn))) &&
|
74
|
+
!this.cache[link],
|
75
|
+
);
|
76
|
+
});
|
77
|
+
|
78
|
+
const communityLinks = changelogIndex.community
|
79
|
+
.map((post) => post.image)
|
80
|
+
.filter(
|
81
|
+
(link) =>
|
82
|
+
link &&
|
83
|
+
(link.startsWith(GITHUB_CDN) || CHECK_CDN.some((cdn) => link.startsWith(cdn))) &&
|
84
|
+
!this.cache[link],
|
85
|
+
) as string[];
|
86
|
+
|
87
|
+
const cloudLinks = changelogIndex.cloud
|
88
|
+
.map((post) => post.image)
|
89
|
+
.filter(
|
90
|
+
(link) =>
|
91
|
+
link &&
|
92
|
+
(link.startsWith(GITHUB_CDN) || CHECK_CDN.some((cdn) => link.startsWith(cdn))) &&
|
93
|
+
!this.cache[link],
|
94
|
+
) as string[];
|
95
|
+
|
96
|
+
// 合并和去重链接数组
|
97
|
+
return mergeAndDeduplicateArrays(links.flat().concat(communityLinks, cloudLinks));
|
98
|
+
}
|
99
|
+
|
100
|
+
// 上传图片到 CDN
|
101
|
+
private async uploadImagesToCDN(links: string[]) {
|
102
|
+
const cdnLinks: { [link: string]: string } = {};
|
103
|
+
|
104
|
+
await pMap(links, async (link) => {
|
105
|
+
consola.start('Uploading image to CDN', link);
|
106
|
+
const file = await fetchImageAsFile(link, 1600);
|
107
|
+
|
108
|
+
if (!file) {
|
109
|
+
consola.error('Failed to fetch image as file', link);
|
110
|
+
return;
|
111
|
+
}
|
112
|
+
|
113
|
+
const cdnUrl = await this.uploadFileToCDN(file, link);
|
114
|
+
if (cdnUrl) {
|
115
|
+
consola.success(link, '>>>', cdnUrl);
|
116
|
+
cdnLinks[link] = cdnUrl.replaceAll(process.env.DOC_S3_PUBLIC_DOMAIN || '', '');
|
117
|
+
}
|
118
|
+
});
|
119
|
+
|
120
|
+
// 更新缓存
|
121
|
+
this.cache = { ...this.cache, ...cdnLinks };
|
122
|
+
this.writeCache();
|
123
|
+
}
|
124
|
+
|
125
|
+
// 根据不同的 CDN 来处理文件上传
|
126
|
+
private async uploadFileToCDN(file: File, link: string): Promise<string | undefined> {
|
127
|
+
if (link.startsWith(GITHUB_CDN)) {
|
128
|
+
const filename = link.replaceAll(GITHUB_CDN, '');
|
129
|
+
return uploader(file, filename);
|
130
|
+
} else if (CHECK_CDN.some((cdn) => link.startsWith(cdn))) {
|
131
|
+
const buffer = await file.arrayBuffer();
|
132
|
+
const hash = createHash('md5').update(Buffer.from(buffer)).digest('hex');
|
133
|
+
return uploader(file, hash);
|
134
|
+
}
|
135
|
+
|
136
|
+
return;
|
137
|
+
}
|
138
|
+
|
139
|
+
// 替换文章中的图片链接
|
140
|
+
private replaceLinksInPosts() {
|
141
|
+
let count = 0;
|
142
|
+
|
143
|
+
for (const post of posts) {
|
144
|
+
const mdx = readFileSync(post, 'utf8');
|
145
|
+
let { content, data } = matter(mdx);
|
146
|
+
const inlineLinks = extractHttpsLinks(content);
|
147
|
+
|
148
|
+
for (const link of inlineLinks) {
|
149
|
+
if (this.cache[link]) {
|
150
|
+
content = content.replaceAll(link, this.cache[link]);
|
151
|
+
count++;
|
152
|
+
}
|
153
|
+
}
|
154
|
+
|
155
|
+
// 更新特定字段的图片链接
|
156
|
+
|
157
|
+
if (data['image'] && this.cache[data['image']]) {
|
158
|
+
data['image'] = this.cache[data['image']];
|
159
|
+
count++;
|
160
|
+
}
|
161
|
+
|
162
|
+
if (data['seo']?.['image'] && this.cache[data['seo']?.['image']]) {
|
163
|
+
data['seo']['image'] = this.cache[data['seo']['image']];
|
164
|
+
count++;
|
165
|
+
}
|
166
|
+
|
167
|
+
writeFileSync(post, matter.stringify(content, data));
|
168
|
+
}
|
169
|
+
|
170
|
+
consola.success(`${count} images have been uploaded to CDN and links have been replaced`);
|
171
|
+
}
|
172
|
+
|
173
|
+
private replaceLinksInChangelogIndex() {
|
174
|
+
let count = 0;
|
175
|
+
changelogIndex.community = changelogIndex.community.map((post) => {
|
176
|
+
if (!post.image) return post;
|
177
|
+
count++;
|
178
|
+
return {
|
179
|
+
...post,
|
180
|
+
image: this.cache[post.image] || post.image,
|
181
|
+
};
|
182
|
+
});
|
183
|
+
|
184
|
+
changelogIndex.cloud = changelogIndex.cloud.map((post) => {
|
185
|
+
if (!post.image) return post;
|
186
|
+
count++;
|
187
|
+
return {
|
188
|
+
...post,
|
189
|
+
image: this.cache[post.image] || post.image,
|
190
|
+
};
|
191
|
+
});
|
192
|
+
|
193
|
+
writeJSONSync(changelogIndexPath, changelogIndex, { spaces: 2 });
|
194
|
+
|
195
|
+
consola.success(
|
196
|
+
`${count} changelog index images have been uploaded to CDN and links have been replaced`,
|
197
|
+
);
|
198
|
+
}
|
199
|
+
|
200
|
+
// 运行上传过程
|
201
|
+
async run() {
|
202
|
+
const links = this.collectImageLinks();
|
203
|
+
|
204
|
+
if (links.length > 0) {
|
205
|
+
consola.info("Found images that haven't been uploaded to CDN:");
|
206
|
+
consola.info(links);
|
207
|
+
await this.uploadImagesToCDN(links);
|
208
|
+
} else {
|
209
|
+
consola.info('No new images to upload.');
|
210
|
+
}
|
211
|
+
}
|
212
|
+
}
|
213
|
+
|
214
|
+
// 实例化并运行
|
215
|
+
const instance = new ImageCDNUploader();
|
216
|
+
|
217
|
+
instance.run();
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import sharp from 'sharp';
|
2
|
+
|
3
|
+
const WIDTH = 1600;
|
4
|
+
|
5
|
+
export const opimized = async (
|
6
|
+
inputBuffer: ArrayBuffer,
|
7
|
+
width: number = WIDTH,
|
8
|
+
): Promise<Buffer> => {
|
9
|
+
return await sharp(inputBuffer)
|
10
|
+
.resize({ width: width, withoutEnlargement: true })
|
11
|
+
.webp()
|
12
|
+
.toBuffer();
|
13
|
+
};
|
14
|
+
|
15
|
+
export const opimizedGif = async (inputBuffer: ArrayBuffer): Promise<Buffer> => {
|
16
|
+
try {
|
17
|
+
return await sharp(inputBuffer, { animated: true }).webp().toBuffer();
|
18
|
+
} catch {
|
19
|
+
return await sharp(inputBuffer, { animated: true, limitInputPixels: false }).webp().toBuffer();
|
20
|
+
}
|
21
|
+
};
|
@@ -0,0 +1,120 @@
|
|
1
|
+
import {
|
2
|
+
GetObjectCommand,
|
3
|
+
PutObjectCommand,
|
4
|
+
PutObjectCommandOutput,
|
5
|
+
S3Client,
|
6
|
+
S3ClientConfig,
|
7
|
+
} from '@aws-sdk/client-s3';
|
8
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
9
|
+
|
10
|
+
import type { ImgInfo, S3UserConfig, UploadResult } from './types';
|
11
|
+
import { extractInfo } from './utils';
|
12
|
+
|
13
|
+
async function getFileURL(
|
14
|
+
opts: createUploadTaskOpts,
|
15
|
+
eTag: string,
|
16
|
+
versionId: string,
|
17
|
+
): Promise<string> {
|
18
|
+
try {
|
19
|
+
const signedUrl = await getSignedUrl(
|
20
|
+
opts.client,
|
21
|
+
new GetObjectCommand({
|
22
|
+
Bucket: opts.bucketName,
|
23
|
+
IfMatch: eTag,
|
24
|
+
Key: opts.path,
|
25
|
+
VersionId: versionId,
|
26
|
+
}),
|
27
|
+
{ expiresIn: 3600 },
|
28
|
+
);
|
29
|
+
const urlObject = new URL(signedUrl);
|
30
|
+
urlObject.search = '';
|
31
|
+
return urlObject.href;
|
32
|
+
} catch (error) {
|
33
|
+
// eslint-disable-next-line unicorn/no-useless-promise-resolve-reject
|
34
|
+
return Promise.reject(error);
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
function createS3Client(opts: S3UserConfig): S3Client {
|
39
|
+
const clientOptions: S3ClientConfig = {
|
40
|
+
credentials: {
|
41
|
+
accessKeyId: opts.accessKeyId,
|
42
|
+
secretAccessKey: opts.secretAccessKey,
|
43
|
+
},
|
44
|
+
|
45
|
+
endpoint: opts.endpoint || undefined,
|
46
|
+
forcePathStyle: opts.pathStyleAccess,
|
47
|
+
region: opts.region || 'auto',
|
48
|
+
};
|
49
|
+
|
50
|
+
const client = new S3Client(clientOptions);
|
51
|
+
return client;
|
52
|
+
}
|
53
|
+
|
54
|
+
interface createUploadTaskOpts {
|
55
|
+
acl: string;
|
56
|
+
bucketName: string;
|
57
|
+
client: S3Client;
|
58
|
+
item: ImgInfo;
|
59
|
+
path: string;
|
60
|
+
urlPrefix?: string;
|
61
|
+
}
|
62
|
+
|
63
|
+
async function createUploadTask(opts: createUploadTaskOpts): Promise<UploadResult> {
|
64
|
+
if (!opts.item.buffer) {
|
65
|
+
throw new Error('undefined image');
|
66
|
+
}
|
67
|
+
|
68
|
+
let body: Buffer;
|
69
|
+
let contentType: string;
|
70
|
+
let contentEncoding: string;
|
71
|
+
|
72
|
+
try {
|
73
|
+
({ body, contentType, contentEncoding } = (await extractInfo(opts.item)) as any);
|
74
|
+
} catch (error) {
|
75
|
+
// eslint-disable-next-line unicorn/no-useless-promise-resolve-reject
|
76
|
+
return Promise.reject(error);
|
77
|
+
}
|
78
|
+
|
79
|
+
const command = new PutObjectCommand({
|
80
|
+
ACL: opts.acl as any,
|
81
|
+
Body: body,
|
82
|
+
Bucket: opts.bucketName,
|
83
|
+
ContentEncoding: contentEncoding,
|
84
|
+
ContentType: contentType,
|
85
|
+
Key: opts.path,
|
86
|
+
});
|
87
|
+
|
88
|
+
let output: PutObjectCommandOutput;
|
89
|
+
try {
|
90
|
+
output = await opts.client.send(command);
|
91
|
+
} catch (error) {
|
92
|
+
// eslint-disable-next-line unicorn/no-useless-promise-resolve-reject
|
93
|
+
return Promise.reject(error);
|
94
|
+
}
|
95
|
+
|
96
|
+
let url: string;
|
97
|
+
if (opts.urlPrefix) {
|
98
|
+
url = `${opts.urlPrefix}/${opts.path}`;
|
99
|
+
} else {
|
100
|
+
try {
|
101
|
+
url = await getFileURL(opts, output.ETag as string, output.VersionId as string);
|
102
|
+
} catch (error) {
|
103
|
+
// eslint-disable-next-line unicorn/no-useless-promise-resolve-reject
|
104
|
+
return Promise.reject(error);
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
return {
|
109
|
+
eTag: output.ETag,
|
110
|
+
imgURL: url,
|
111
|
+
key: opts.path,
|
112
|
+
url: url,
|
113
|
+
versionId: output.VersionId,
|
114
|
+
};
|
115
|
+
}
|
116
|
+
|
117
|
+
export default {
|
118
|
+
createS3Client,
|
119
|
+
createUploadTask,
|
120
|
+
};
|
@@ -0,0 +1,25 @@
|
|
1
|
+
export interface ImgInfo {
|
2
|
+
[propName: string]: any;
|
3
|
+
buffer: Buffer;
|
4
|
+
extname: string;
|
5
|
+
fileName: string;
|
6
|
+
}
|
7
|
+
|
8
|
+
export interface S3UserConfig {
|
9
|
+
accessKeyId: string;
|
10
|
+
bucketName: string;
|
11
|
+
endpoint: string;
|
12
|
+
pathPrefix: string;
|
13
|
+
pathStyleAccess?: boolean;
|
14
|
+
region: string;
|
15
|
+
secretAccessKey: string;
|
16
|
+
uploadPath?: string;
|
17
|
+
}
|
18
|
+
|
19
|
+
export interface UploadResult {
|
20
|
+
eTag?: string;
|
21
|
+
imgURL: string;
|
22
|
+
key: string;
|
23
|
+
url: string;
|
24
|
+
versionId?: string;
|
25
|
+
}
|
@@ -0,0 +1,106 @@
|
|
1
|
+
import CryptoJS from 'crypto-js';
|
2
|
+
import mime from 'mime';
|
3
|
+
|
4
|
+
import { ImgInfo } from './types';
|
5
|
+
|
6
|
+
class FileNameGenerator {
|
7
|
+
date: Date;
|
8
|
+
info: ImgInfo;
|
9
|
+
|
10
|
+
static fields = [
|
11
|
+
'year',
|
12
|
+
'month',
|
13
|
+
'day',
|
14
|
+
'fullName',
|
15
|
+
'fileName',
|
16
|
+
'extName',
|
17
|
+
'timestamp',
|
18
|
+
'timestampMS',
|
19
|
+
'md5',
|
20
|
+
];
|
21
|
+
|
22
|
+
constructor(info: ImgInfo) {
|
23
|
+
this.date = new Date();
|
24
|
+
this.info = info;
|
25
|
+
}
|
26
|
+
|
27
|
+
public year(): string {
|
28
|
+
return `${this.date.getFullYear()}`;
|
29
|
+
}
|
30
|
+
|
31
|
+
public month(): string {
|
32
|
+
return this.date.getMonth() < 9
|
33
|
+
? `0${this.date.getMonth() + 1}`
|
34
|
+
: `${this.date.getMonth() + 1}`;
|
35
|
+
}
|
36
|
+
|
37
|
+
public day(): string {
|
38
|
+
return this.date.getDate() < 9 ? `0${this.date.getDate()}` : `${this.date.getDate()}`;
|
39
|
+
}
|
40
|
+
|
41
|
+
public fullName(): string {
|
42
|
+
return this.info.fileName;
|
43
|
+
}
|
44
|
+
|
45
|
+
public fileName(): string {
|
46
|
+
return this.info.fileName.replace(this.info.extname, '');
|
47
|
+
}
|
48
|
+
|
49
|
+
public extName(): string {
|
50
|
+
return this.info.extname.replace('.', '');
|
51
|
+
}
|
52
|
+
|
53
|
+
public timestamp(): string {
|
54
|
+
return Math.floor(Date.now() / 1000).toString();
|
55
|
+
}
|
56
|
+
|
57
|
+
public timestampMS(): string {
|
58
|
+
return Date.now().toString();
|
59
|
+
}
|
60
|
+
|
61
|
+
public md5(): string {
|
62
|
+
const wordArray = CryptoJS.lib.WordArray.create(this.imgBuffer());
|
63
|
+
const md5Hash = CryptoJS.MD5(wordArray);
|
64
|
+
return md5Hash.toString(CryptoJS.enc.Hex);
|
65
|
+
}
|
66
|
+
private imgBuffer(): Buffer {
|
67
|
+
return this.info.buffer;
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
export function formatPath(info: ImgInfo, format?: string): string {
|
72
|
+
if (!format) {
|
73
|
+
return info.fileName;
|
74
|
+
}
|
75
|
+
|
76
|
+
const fileNameGenerator = new FileNameGenerator(info);
|
77
|
+
|
78
|
+
let formatPath: string = format;
|
79
|
+
|
80
|
+
for (const key of FileNameGenerator.fields) {
|
81
|
+
const re = new RegExp(`{${key}}`, 'g');
|
82
|
+
// @ts-ignore
|
83
|
+
formatPath = formatPath.replace(re, fileNameGenerator[key]());
|
84
|
+
}
|
85
|
+
|
86
|
+
return formatPath;
|
87
|
+
}
|
88
|
+
|
89
|
+
export async function extractInfo(info: ImgInfo): Promise<{
|
90
|
+
body?: Buffer;
|
91
|
+
contentEncoding?: string;
|
92
|
+
contentType?: string;
|
93
|
+
}> {
|
94
|
+
const result: {
|
95
|
+
body?: Buffer;
|
96
|
+
contentEncoding?: string;
|
97
|
+
contentType?: string;
|
98
|
+
} = {};
|
99
|
+
|
100
|
+
if (info.extname) {
|
101
|
+
result.contentType = mime.getType(info.extname) as string;
|
102
|
+
}
|
103
|
+
result.body = info.buffer;
|
104
|
+
|
105
|
+
return result;
|
106
|
+
}
|
@@ -0,0 +1,73 @@
|
|
1
|
+
import { consola } from 'consola';
|
2
|
+
import dotenv from 'dotenv';
|
3
|
+
|
4
|
+
import s3 from './s3';
|
5
|
+
import type { ImgInfo, S3UserConfig, UploadResult } from './s3/types';
|
6
|
+
import { formatPath } from './s3/utils';
|
7
|
+
|
8
|
+
dotenv.config();
|
9
|
+
|
10
|
+
if (!process.env.DOC_S3_ACCESS_KEY_ID) {
|
11
|
+
consola.error('请配置 Doc S3 存储的环境变量: DOC_S3_ACCESS_KEY_ID');
|
12
|
+
// eslint-disable-next-line unicorn/no-process-exit
|
13
|
+
process.exit(1);
|
14
|
+
}
|
15
|
+
|
16
|
+
if (!process.env.DOC_S3_SECRET_ACCESS_KEY) {
|
17
|
+
consola.error('请配置 Doc S3 存储的环境变量: DOC_S3_SECRET_ACCESS_KEY');
|
18
|
+
// eslint-disable-next-line unicorn/no-process-exit
|
19
|
+
process.exit(1);
|
20
|
+
}
|
21
|
+
|
22
|
+
if (!process.env.DOC_S3_PUBLIC_DOMAIN) {
|
23
|
+
consola.error('请配置 Doc S3 存储的环境变量: DOC_S3_PUBLIC_DOMAIN');
|
24
|
+
// eslint-disable-next-line unicorn/no-process-exit
|
25
|
+
process.exit(1);
|
26
|
+
}
|
27
|
+
|
28
|
+
export const BASE_PATH = 'blog/assets';
|
29
|
+
|
30
|
+
export const uploader = async (
|
31
|
+
file: File,
|
32
|
+
filename: string,
|
33
|
+
basePath: string = BASE_PATH,
|
34
|
+
uploadPath?: string,
|
35
|
+
) => {
|
36
|
+
const item: ImgInfo = {
|
37
|
+
buffer: Buffer.from(await file.arrayBuffer()),
|
38
|
+
extname: file.name.split('.').pop() as string,
|
39
|
+
fileName: file.name,
|
40
|
+
mimeType: file.type,
|
41
|
+
};
|
42
|
+
|
43
|
+
const userConfig: S3UserConfig = {
|
44
|
+
accessKeyId: process.env.DOC_S3_ACCESS_KEY_ID || '',
|
45
|
+
bucketName: 'hub-apac-1',
|
46
|
+
endpoint: 'https://d35842305b91be4b48e06ff9a9ad83f5.r2.cloudflarestorage.com',
|
47
|
+
pathPrefix: process.env.DOC_S3_PUBLIC_DOMAIN || '',
|
48
|
+
pathStyleAccess: true,
|
49
|
+
region: 'auto',
|
50
|
+
secretAccessKey: process.env.DOC_S3_SECRET_ACCESS_KEY || '',
|
51
|
+
uploadPath: uploadPath || `${basePath}${filename}.{extName}`,
|
52
|
+
};
|
53
|
+
|
54
|
+
const client = s3.createS3Client(userConfig);
|
55
|
+
|
56
|
+
let results: UploadResult;
|
57
|
+
|
58
|
+
try {
|
59
|
+
results = await s3.createUploadTask({
|
60
|
+
acl: 'public-read',
|
61
|
+
bucketName: userConfig.bucketName,
|
62
|
+
client,
|
63
|
+
item: item,
|
64
|
+
path: formatPath(item, userConfig.uploadPath),
|
65
|
+
urlPrefix: userConfig.pathPrefix,
|
66
|
+
});
|
67
|
+
|
68
|
+
return results.url;
|
69
|
+
} catch (error) {
|
70
|
+
consola.error('上传到 S3 存储发生错误,请检查网络连接和配置是否正确');
|
71
|
+
consola.error(error);
|
72
|
+
}
|
73
|
+
};
|
@@ -0,0 +1,93 @@
|
|
1
|
+
import { readJSONSync } from 'fs-extra';
|
2
|
+
import { globSync } from 'glob';
|
3
|
+
import { resolve } from 'node:path';
|
4
|
+
|
5
|
+
import { opimized, opimizedGif } from './optimized';
|
6
|
+
|
7
|
+
export const fixWinPath = (path: string) => path.replaceAll('\\', '/');
|
8
|
+
|
9
|
+
export const root = resolve(__dirname, '../..');
|
10
|
+
|
11
|
+
export const posts = globSync(fixWinPath(resolve(root, 'docs/changelog/*.mdx')));
|
12
|
+
|
13
|
+
interface ChangelogItem {
|
14
|
+
date: string;
|
15
|
+
id: string;
|
16
|
+
image?: string;
|
17
|
+
versionRange: string[];
|
18
|
+
}
|
19
|
+
|
20
|
+
export const changelogIndexPath = resolve(root, 'docs/changelog/index.json');
|
21
|
+
|
22
|
+
export const changelogIndex: {
|
23
|
+
cloud: ChangelogItem[];
|
24
|
+
community: ChangelogItem[];
|
25
|
+
} = readJSONSync(changelogIndexPath);
|
26
|
+
|
27
|
+
export const extractHttpsLinks = (text: string) => {
|
28
|
+
const regex = /https:\/\/[^\s"')>]+/g;
|
29
|
+
const links = text.match(regex);
|
30
|
+
return links || [];
|
31
|
+
};
|
32
|
+
|
33
|
+
export const mergeAndDeduplicateArrays = (...arrays: string[][]) => {
|
34
|
+
const combinedArray = arrays.flat();
|
35
|
+
const uniqueSet = new Set(combinedArray);
|
36
|
+
return Array.from(uniqueSet);
|
37
|
+
};
|
38
|
+
|
39
|
+
const mimeToExtensions = {
|
40
|
+
'image/gif': '.gif',
|
41
|
+
// 图片类型
|
42
|
+
'image/jpeg': '.jpg',
|
43
|
+
'image/png': '.png',
|
44
|
+
'image/svg+xml': '.svg',
|
45
|
+
'image/webp': '.webp',
|
46
|
+
// 视频类型
|
47
|
+
'video/mp4': '.mp4',
|
48
|
+
'video/mpeg': '.mpeg',
|
49
|
+
'video/ogg': '.ogv',
|
50
|
+
'video/quicktime': '.mov',
|
51
|
+
'video/webm': '.webm',
|
52
|
+
'video/x-flv': '.flv',
|
53
|
+
'video/x-matroska': '.mkv',
|
54
|
+
'video/x-ms-wmv': '.wmv',
|
55
|
+
'video/x-msvideo': '.avi',
|
56
|
+
};
|
57
|
+
|
58
|
+
// @ts-ignore
|
59
|
+
const getExtension = (type: string) => mimeToExtensions?.[type] || '.png';
|
60
|
+
|
61
|
+
export const fetchImageAsFile = async (url: string, width: number) => {
|
62
|
+
try {
|
63
|
+
// Step 1: Fetch the image
|
64
|
+
const response = await fetch(url);
|
65
|
+
if (!response.ok) {
|
66
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
67
|
+
}
|
68
|
+
|
69
|
+
// Step 2: Create a blob from the response data
|
70
|
+
const blob = await response.blob();
|
71
|
+
let buffer: ArrayBuffer | Buffer = await blob.arrayBuffer();
|
72
|
+
let type = getExtension(blob.type);
|
73
|
+
if (type === '.gif') {
|
74
|
+
buffer = await opimizedGif(buffer);
|
75
|
+
type = '.webp';
|
76
|
+
} else if (type === '.png' || type === '.jpg') {
|
77
|
+
buffer = await opimized(buffer, width);
|
78
|
+
type = '.webp';
|
79
|
+
}
|
80
|
+
|
81
|
+
const filename = Date.now().toString() + type;
|
82
|
+
|
83
|
+
// Step 3: Create a file from the blob
|
84
|
+
const file: File = new File([buffer], filename, {
|
85
|
+
lastModified: Date.now(),
|
86
|
+
type: type === '.webp' ? 'image/webp' : blob.type,
|
87
|
+
});
|
88
|
+
|
89
|
+
return file;
|
90
|
+
} catch (error) {
|
91
|
+
console.error('Error fetching image as file:', error);
|
92
|
+
}
|
93
|
+
};
|