From 50144be36ede30aa6d9d5b6f1bc5d4a6bd219414 Mon Sep 17 00:00:00 2001 From: keepchen Date: Thu, 26 Feb 2026 13:53:08 +0800 Subject: [PATCH] feature:uploader add StarDots provider --- package.json | 1 + pnpm-lock.yaml | 8 + .../universal-image-uploader/banner.txt | 7 +- .../universal-image-uploader/constants.ts | 39 ++++ .../universal-image-uploader/index.ts | 206 +++++++++++++++++- .../universal-image-uploader/utils.ts | 5 + 6 files changed, 260 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 85a94da..26f4373 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "browser-extension-settings": "^0.8.10", "browser-extension-storage": "^0.2.10", "browser-extension-utils": "^0.3.4", + "crypto-js": "^4.2.0", "tailwindcss": "^4.1.18" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4af8af1..5c4db1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: browser-extension-utils: specifier: ^0.3.4 version: 0.3.4 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 tailwindcss: specifier: ^4.1.18 version: 4.1.18 @@ -1462,6 +1465,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + css-declaration-sorter@7.3.0: resolution: {integrity: sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ==} engines: {node: ^14 || ^16 || >=18} @@ -4835,6 +4841,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-js@4.2.0: {} + css-declaration-sorter@7.3.0(postcss@8.5.6): dependencies: postcss: 8.5.6 diff --git a/src/packages/universal-image-uploader/banner.txt b/src/packages/universal-image-uploader/banner.txt index 63a16bc..a16f7c2 100644 --- a/src/packages/universal-image-uploader/banner.txt +++ b/src/packages/universal-image-uploader/banner.txt @@ -6,9 +6,9 @@ // @homepageURL {homepage} // @supportURL {bugs.url} // @version 0.13.4 -// @description Paste/drag/select images, batch upload to Imgur/Tikolu/MJJ.Today/Appinn; auto-copy Markdown/HTML/BBCode/link; site button integration with SPA observer; local history. -// @description:zh-CN 通用图片上传与插入:支持粘贴/拖拽/选择,批量上传至 Imgur/Tikolu/MJJ.Today/Appinn;自动复制 Markdown/HTML/BBCode/链接;可为各站点插入按钮并适配 SPA;保存本地历史。 -// @description:zh-TW 通用圖片上傳與插入:支援貼上/拖曳/選擇,批次上傳至 Imgur/Tikolu/MJJ.Today/Appinn;自動複製 Markdown/HTML/BBCode/連結;可為各站點插入按鈕並適配 SPA;保存本地歷史。 +// @description Paste/drag/select images, batch upload to Imgur/Tikolu/MJJ.Today/Appinn/StarDots; auto-copy Markdown/HTML/BBCode/link; site button integration with SPA observer; local history. +// @description:zh-CN 通用图片上传与插入:支持粘贴/拖拽/选择,批量上传至 Imgur/Tikolu/MJJ.Today/Appinn/StarDots;自动复制 Markdown/HTML/BBCode/链接;可为各站点插入按钮并适配 SPA;保存本地历史。 +// @description:zh-TW 通用圖片上傳與插入:支援貼上/拖曳/選擇,批次上傳至 Imgur/Tikolu/MJJ.Today/Appinn/StarDots;自動複製 Markdown/HTML/BBCode/連結;可為各站點插入按鈕並適配 SPA;保存本地歷史。 // @author {author} // @license {license} // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSI+PHJlY3QgeD0iOCIgeT0iOCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiByeD0iMTAiIHN0cm9rZT0iIzFmMjkzNyIgc3Ryb2tlLXdpZHRoPSI0Ii8+PHBhdGggZD0iTTMyIDIwbC0xMiAxMmg3djE4aDEwVjMyaDdsLTEyLTEyeiIgZmlsbD0iIzFmMjkzNyIvPjwvc3ZnPg== @@ -27,5 +27,6 @@ // @connect photo.lily.lat // @connect i.111666.best // @connect skyimg.net +// @connect api.stardots.io // ==/UserScript== // diff --git a/src/packages/universal-image-uploader/constants.ts b/src/packages/universal-image-uploader/constants.ts index cdf3bf8..6e26c94 100644 --- a/src/packages/universal-image-uploader/constants.ts +++ b/src/packages/universal-image-uploader/constants.ts @@ -134,6 +134,7 @@ export const I18N: Record = { host_111666_best: '111666.best', host_skyimg: 'Skyimg', host_skyimg_webp: 'Skyimg (WebP)', + host_stardots: 'StarDots', btn_select_images: 'Select images', progress_initial: 'Done 0/0', progress_done: 'Done {done}/{total}', @@ -190,6 +191,18 @@ export const I18N: Record = { error_upload_failed: 'Upload failed', placeholder_uploading: 'Uploading "{name}"...', placeholder_upload_failed: 'Upload failed: {name}', + stardots_config_title: 'StarDots API Configration', + stardots_key_title: 'API Key', + stardots_key_placeholder: 'Please Enter API Key', + stardots_secret_title: 'API Secret', + stardots_secret_placeholder: 'Please Enter API Secret', + stardots_bucket_title: 'Bucket', + stardots_bucket_placeholder: 'Please Enter Bucket', + stardots_save_title: 'Save', + stardots_save_result_title: '✅ Saved', + stardots_get_credentials_title: 'Get StarDots credentails', + stardots_set_config_tips: + 'Please select StarDots and complete the configuration.', }, 'zh-CN': { header_title: '通用图片上传助手', @@ -209,6 +222,7 @@ export const I18N: Record = { host_111666_best: '111666.best', host_skyimg: 'Skyimg', host_skyimg_webp: 'Skyimg (WebP)', + host_stardots: 'StarDots', btn_select_images: '选择图片', progress_initial: '完成 0/0', progress_done: '完成 {done}/{total}', @@ -264,6 +278,17 @@ export const I18N: Record = { error_upload_failed: '上传失败', placeholder_uploading: '正在上传「{name}」...', placeholder_upload_failed: '上传失败:{name}', + stardots_config_title: 'StarDots API 配置', + stardots_key_title: 'API Key', + stardots_key_placeholder: '请输入 API Key', + stardots_secret_title: 'API Secret', + stardots_secret_placeholder: '请输入 API Secret', + stardots_bucket_title: '存储桶', + stardots_bucket_placeholder: '请输入 存储桶', + stardots_save_title: '保存', + stardots_save_result_title: '✅ 保存成功', + stardots_get_credentials_title: '获取 StarDots 凭据', + stardots_set_config_tips: '请选择 StarDots 并完成配置。', }, 'zh-TW': { header_title: '通用圖片上傳助手', @@ -283,6 +308,7 @@ export const I18N: Record = { host_111666_best: '111666.best', host_skyimg: 'Skyimg', host_skyimg_webp: 'Skyimg (WebP)', + host_stardots: 'StarDots', btn_select_images: '選擇圖片', progress_initial: '完成 0/0', progress_done: '完成 {done}/{total}', @@ -338,6 +364,17 @@ export const I18N: Record = { error_upload_failed: '上傳失敗', placeholder_uploading: '正在上傳「{name}」...', placeholder_upload_failed: '上傳失敗:{name}', + stardots_config_title: 'StarDots API 設定', + stardots_key_title: 'API Key', + stardots_key_placeholder: '請輸入 API Key', + stardots_secret_title: 'API Secret', + stardots_secret_placeholder: '請輸入 API Secret', + stardots_bucket_title: '儲存桶', + stardots_bucket_placeholder: '請輸入 儲存桶', + stardots_save_title: '儲存', + stardots_save_result_title: '✅ 儲存成功', + stardots_get_credentials_title: '獲取 StarDots 憑據', + stardots_set_config_tips: '請選擇 StarDots 並完成設定。', }, } @@ -375,6 +412,7 @@ export const ALLOWED_HOSTS = ENABLE_MOCK_HOST '111666_best', 'mock', 'mock2', + 'stardots', ] : [ 'imgur', @@ -386,6 +424,7 @@ export const ALLOWED_HOSTS = ENABLE_MOCK_HOST 'skyimg_webp', 'photo_lily', '111666_best', + 'stardots', ] export const ALLOWED_PROXIES = [ 'none', diff --git a/src/packages/universal-image-uploader/index.ts b/src/packages/universal-image-uploader/index.ts index 813bdc4..884763d 100644 --- a/src/packages/universal-image-uploader/index.ts +++ b/src/packages/universal-image-uploader/index.ts @@ -39,6 +39,7 @@ import { ensureAllowedValue, getAllowedFormats, getCustomFormats, + md5Encode, normalizeHost, removeCustomFormat, setCustomFormats, @@ -573,6 +574,94 @@ const buildFormatOptions = async (selectEl, selectedValue) => { } } +var STARDOTS_CONFIG = { key: '', secret: '', bucket: '' } + +const injectStarDotsSettings = () => { + const hostEl = document.querySelector('#uiu-panel') + if (!hostEl) return + const shadowRoot = hostEl.shadowRoot + if (!shadowRoot) return + const container = shadowRoot.querySelectorAll('.uiu-body .uiu-controls')[1] + const wrapper = document.createElement('div') + wrapper.innerHTML = ` +
+

${t('stardots_config_title')}

+ + + + + + + + + +
+ + + ${t('stardots_get_credentials_title')} +
+
+` + const scsEl = container.querySelector('#sd-configuration-section') + if (scsEl) { + scsEl.remove() + } + + container.appendChild(wrapper) + + loadStarDotsConfig() + + shadowRoot + .getElementById('sd-save-config') + ?.addEventListener('click', async () => { + const key = ( + shadowRoot.getElementById('sd-api-key') as HTMLInputElement + )?.value.trim() + const secret = ( + shadowRoot.getElementById('sd-api-secret') as HTMLInputElement + )?.value.trim() + const bucket = ( + shadowRoot.getElementById('sd-bucket') as HTMLInputElement + )?.value.trim() + + const targetOrigin: string = '*' + window.postMessage( + { + type: 'uiu:stardots-save-config', + payload: { key, secret, bucket }, + }, + targetOrigin + ) + + const statusEl = shadowRoot.getElementById( + 'sd-save-status' + ) as HTMLSpanElement + if (statusEl) { + statusEl.innerText = t('stardots_save_result_title') + } + + setTimeout(() => { + if (statusEl) { + statusEl.innerText = '' + } + const configSection = shadowRoot.getElementById( + 'sd-configuration-section' + ) as HTMLDivElement + if (configSection) { + configSection.style.display = 'none' + } + }, 2000) + }) +} +var loadStarDotsConfig = async () => { + window.postMessage( + { + type: 'uiu:stardots-get-config', + }, + '*' + ) +} + // Helper: build host options const buildHostOptions = (selectEl, selectedValue) => { if (!selectEl) return @@ -586,6 +675,14 @@ const buildHostOptions = (selectEl, selectedValue) => { if (val === selected) opt.selected = true selectEl.append(opt) } + selectEl.onchange = (event: Event) => { + const selectedValue = (event.target as HTMLSelectElement)?.value.trim() + if (selectedValue === 'stardots') { + injectStarDotsSettings() + } else { + //TODO other logic + } + } } const buildSecondaryHostOptions = ( @@ -608,6 +705,14 @@ const buildSecondaryHostOptions = ( if (val === selected) opt.selected = true selectEl.append(opt) } + selectEl.onchange = (event: Event) => { + const selectedValue = (event.target as HTMLSelectElement)?.value.trim() + if (selectedValue === 'stardots') { + injectStarDotsSettings() + } else { + //TODO other logic + } + } } // Helper: get proxy label key @@ -650,7 +755,7 @@ const css = ` #uiu-panel .uiu-body { padding: 8px 12px; } #uiu-panel .uiu-controls { display:flex; align-items:center; gap:8px; flex-wrap: wrap; } #uiu-panel .uiu-controls label { display:inline-flex; align-items:center; } - #uiu-panel select, #uiu-panel button { font-size: 12px; padding: 6px 10px; border-radius: 6px; border: 1px solid #334155; background:#1f2937; color:#fff; } + #uiu-panel select, #uiu-panel button, #uiu-panel input { font-size: 12px; padding: 6px 10px; border-radius: 6px; border: 1px solid #334155; background:#1f2937; color:#fff; } #uiu-panel button.uiu-primary { background:#2563eb; border-color:#1d4ed8; } #uiu-panel .uiu-list { margin-top:8px; max-height: 140px; overflow-y:auto; overflow-x:hidden; font-size: 12px; } #uiu-panel .uiu-list .uiu-item { padding:6px 0; border-bottom: 1px dashed #334155; white-space: normal; word-break: break-word; overflow-wrap: anywhere; } @@ -1115,6 +1220,56 @@ async function uploadToTikolu(file) { throw new Error(t('error_upload_failed')) } +const STARDOTS_ENDPOINT = 'https://api.stardots.io' +async function uploadToStarDots(file) { + const apiKey = STARDOTS_CONFIG.key + const apiSecret = STARDOTS_CONFIG.secret + const bucket = STARDOTS_CONFIG.bucket + + if (!apiKey || !apiSecret) { + alert(t('stardots_set_config_tips')) + throw new Error('Missing credentials') + } + + const timestamp = parseInt((Date.now() / 1000).toString()).toString() + const nonce = `${Date.now()}${Math.random().toString(16)}` + .replace('.', '') + .slice(0, 20) + + const stringToSign = `${timestamp}|${apiSecret}|${nonce}` + const signature = md5Encode(stringToSign).toUpperCase() + + console.log(timestamp, nonce, stringToSign, signature) + + const form = new FormData() + form.append('file', file) + form.append('filename', file.name) + form.append('space', bucket) + + try { + const data = await gmRequest({ + method: 'PUT', + url: `${STARDOTS_ENDPOINT}/openapi/file/upload`, + headers: { + 'x-stardots-key': apiKey, + 'x-stardots-nonce': nonce, + 'x-stardots-timestamp': timestamp, + 'x-stardots-sign': signature, + 'x-stardots-assistant-version': 'upload-by-utags', + }, + data: form, + responseType: 'json', + }) + if (data.success) { + return data.data.url + } + throw new Error(`${data.message}(${data.requestId})`) + } catch (e) { + console.log('stardots upload error', e) + throw new Error(t('error_upload_failed')) + } +} + async function uploadImageToHost(file, host: string) { if (host === 'mock' || host === 'mock2') { await new Promise((resolve) => { @@ -1138,6 +1293,7 @@ async function uploadImageToHost(file, host: string) { if (host === 'appinn') return uploadToAppinn(file) if (host === 'photo_lily') return uploadToPhotoLily(file) if (host === '111666_best') return uploadTo111666Best(file) + if (host === 'stardots') return uploadToStarDots(file) // Default return uploadToImgur(file) } @@ -1541,7 +1697,7 @@ document.addEventListener( // true // ) -globalThis.addEventListener('message', (event) => { +globalThis.addEventListener('message', async (event) => { const type = event.data?.type switch (type) { case 'uiu:insert-placeholder': { @@ -1565,6 +1721,47 @@ globalThis.addEventListener('message', (event) => { break } + case 'uiu:stardots-save-config': { + const { key, secret, bucket } = event.data.payload + await setValue('stardots_key', key) + await setValue('stardots_secret', secret) + await setValue('stardots_bucket', bucket) + break + } + case 'uiu:stardots-get-config': { + const key = await getValue('stardots_key') + const secret = await getValue('stardots_secret') + const bucket = await getValue('stardots_bucket') + const hostEl = document.querySelector('#uiu-panel') + if (!hostEl) return + const shadowRoot = hostEl.shadowRoot + if (!shadowRoot) return + const keyInput = shadowRoot.getElementById( + 'sd-api-key' + ) as HTMLInputElement + if (keyInput) { + keyInput.value = key ?? '' + } + const secretInput = shadowRoot.getElementById( + 'sd-api-secret' + ) as HTMLInputElement + if (secretInput) { + secretInput.value = secret ?? '' + } + const bucketInput = shadowRoot.getElementById( + 'sd-bucket' + ) as HTMLInputElement + if (bucketInput) { + bucketInput.value = bucket ?? '' + } + STARDOTS_CONFIG = { + key: key || '', + secret: secret || '', + bucket: bucket || '', + } + break + } + default: { break } @@ -2264,7 +2461,7 @@ async function createPanel(): Promise< } catch {} }) - globalThis.addEventListener('message', (event) => { + globalThis.addEventListener('message', async (event) => { const type = event.data?.type switch (type) { case 'iu:uploadFiles': { @@ -2849,5 +3046,8 @@ async function createPanel(): Promise< } ) } + + //init while script loaded + loadStarDotsConfig() } catch {} })() diff --git a/src/packages/universal-image-uploader/utils.ts b/src/packages/universal-image-uploader/utils.ts index 39d9b9d..bd54904 100644 --- a/src/packages/universal-image-uploader/utils.ts +++ b/src/packages/universal-image-uploader/utils.ts @@ -1,4 +1,5 @@ import { getValue, setValue } from 'browser-extension-storage' +import md5 from 'crypto-js/md5' import { ALLOWED_FORMATS, @@ -155,3 +156,7 @@ export async function getAllowedFormats() { export async function ensureAllowedFormat(fmt: any) { return ensureAllowedValue(fmt, await getAllowedFormats(), DEFAULT_FORMAT) } + +export const md5Encode = (str: string) => { + return md5(str).toString() +}