From 93408a7461f97475f34a56d62235dc63431e056d Mon Sep 17 00:00:00 2001 From: YQ <37969157+yqs112358@users.noreply.github.com> Date: Tue, 7 Jul 2026 23:34:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81picgo=20server?= =?UTF-8?q?=E5=9B=BE=E5=BA=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chrome/icons/picgo.png | Bin 0 -> 4578 bytes .../backend/imageHosting/picgo/form.tsx | 35 ++++++ .../backend/imageHosting/picgo/index.ts | 16 +++ .../backend/imageHosting/picgo/service.ts | 104 ++++++++++++++++++ 4 files changed, 155 insertions(+) create mode 100644 chrome/icons/picgo.png create mode 100644 src/common/backend/imageHosting/picgo/form.tsx create mode 100644 src/common/backend/imageHosting/picgo/index.ts create mode 100644 src/common/backend/imageHosting/picgo/service.ts diff --git a/chrome/icons/picgo.png b/chrome/icons/picgo.png new file mode 100644 index 0000000000000000000000000000000000000000..e5aa1a1ecd2dbf06f6755f5ea9d22801aff3581c GIT binary patch literal 4578 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 M07*qoM6N<$f`Japp#T5? literal 0 HcmV?d00001 diff --git a/src/common/backend/imageHosting/picgo/form.tsx b/src/common/backend/imageHosting/picgo/form.tsx new file mode 100644 index 000000000..bc1257813 --- /dev/null +++ b/src/common/backend/imageHosting/picgo/form.tsx @@ -0,0 +1,35 @@ +import React, { Fragment } from 'react'; +import { FormComponentProps } from '@ant-design/compatible/lib/form'; +import { Form } from '@ant-design/compatible'; +import '@ant-design/compatible/assets/index.less'; +import { Input } from 'antd'; + +interface Props extends FormComponentProps { + info: { + uploadUrl: string; + secret: string; + }; +} + +export default ({ form: { getFieldDecorator }, info }: Props) => { + const initInfo: Partial = info || {}; + return ( + + + {getFieldDecorator('uploadUrl', { + initialValue: initInfo.uploadUrl, + rules: [ + { + required: true, + }, + ], + })()} + + + {getFieldDecorator('secret', { + initialValue: initInfo.secret, + })()} + + + ); +}; diff --git a/src/common/backend/imageHosting/picgo/index.ts b/src/common/backend/imageHosting/picgo/index.ts new file mode 100644 index 000000000..8aa94208e --- /dev/null +++ b/src/common/backend/imageHosting/picgo/index.ts @@ -0,0 +1,16 @@ +import Form from './form'; +import { ImageHostingServiceMeta } from '../interface'; +import Service from './service'; + +export default (): ImageHostingServiceMeta => { + return { + name: 'PicGo', + icon: 'icons/picgo.png', + type: 'picgo', + service: Service, + form: Form, + permission: { + origins: [''], + }, + }; +}; diff --git a/src/common/backend/imageHosting/picgo/service.ts b/src/common/backend/imageHosting/picgo/service.ts new file mode 100644 index 000000000..8bf3077ca --- /dev/null +++ b/src/common/backend/imageHosting/picgo/service.ts @@ -0,0 +1,104 @@ +import { IBasicRequestService } from '@/service/common/request'; +import { RequestHelper } from '@/service/request/common/request'; +import { UploadImageRequest, ImageHostingService } from '../interface'; +import { Base64ImageToBlob } from '@/common/blob'; +import Container from 'typedi'; +import md5 from '@web-clipper/shared/lib/md5'; + +export interface PicgoImageHostingOption { + uploadUrl: string; + secret: string; +} + +export default class PicgoImageHostingService implements ImageHostingService { + private config: PicgoImageHostingOption; + + constructor(config: PicgoImageHostingOption) { + this.config = config; + } + + getId = () => { + const secret = this.config.secret ? `:${this.config.secret}` : ''; + return md5(`${this.config.uploadUrl}${secret}`); // as id + }; + + uploadImage = async ({ data }: UploadImageRequest) => { + const blob = Base64ImageToBlob(data); + return this.uploadBlob(blob, `web_cliper_image.png`); + }; + + uploadImageUrl = async (url: string) => { + const imageBlob = await Container.get(IBasicRequestService).download(url); + return this.uploadBlob(imageBlob, this._getImageFileName(url)); + }; + + private uploadBlob = async (blob: Blob, fileName?: string): Promise => { + const request = new RequestHelper({ request: Container.get(IBasicRequestService) }); + const formData = new FormData(); + const headers: { Authorization?: string } = {}; + if (this.config.secret) { + headers.Authorization = `Bearer ${this.config.secret}`; + } + formData.append('files', blob, fileName); + const result = await request.postForm<{ success: boolean; result: string[]; message?: string }>( + this.config.uploadUrl, + { + data: formData, + headers, + } + ); + if (!result.success) throw new Error(result.message || 'Upload failed'); + return result.result[0]; + }; + + private _getImageFileName(url: string) { + // 分割路径和查询参数 + const queryIndex = url.indexOf('?'); + const pathPart = queryIndex === -1 ? url : url.slice(0, queryIndex); + const queryPart = queryIndex === -1 ? '' : url.slice(queryIndex + 1); + + // 处理路径部分 + const segments = pathPart.split('/'); + let lastSegment = segments.pop() || ''; + + // 移除可能的哈希片段 + const hashIndex = lastSegment.indexOf('#'); + if (hashIndex !== -1) { + lastSegment = lastSegment.slice(0, hashIndex); + } + + // 检查最后一段是否为文件名 + if (lastSegment.includes('.')) { + return lastSegment; + } + const fileName = 'web_cliper_image'; + let fileExt: string = 'png'; + // 解析查询参数中的后缀 + const queryParams = new URLSearchParams(queryPart); + const formatKeys = ['wx_fmt', 'format', 'fm', 'type']; + for (const key of formatKeys) { + if (queryParams.has(key)) { + fileExt = queryParams.get(key) as string; + if (fileExt) { + break; + } + } + } + + // 检查路径中的其他段是否有已知图片后缀 + const imageExts = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp']; + for (const seg of segments) { + const dotIndex = seg.lastIndexOf('.'); + if (dotIndex !== -1) { + const ext = seg.slice(dotIndex + 1).toLowerCase(); + if (imageExts.includes(ext)) { + fileExt = ext; + break; + } + } + } + + // 默认返回空字符串 + return `${fileName}.${fileExt}`; + } +}